Skip to main content

typst_pack/
compile.rs

1//! Compiling a pack into Compilation Output Artifacts.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::num::NonZeroUsize;
5use std::ops::Range;
6
7use ecow::EcoVec;
8#[cfg(feature = "parallel")]
9use rayon::prelude::*;
10use typst::diag::{Severity, SourceDiagnostic, Tracepoint, Warned};
11use typst::foundations::{Bytes, Datetime, Dict, Repr, Smart};
12use typst::syntax::package::PackageSpec;
13use typst::syntax::{DiagSpan, Span};
14use typst::{Feature, World, WorldExt};
15use typst_layout::PagedDocument;
16use typst_pdf::{PdfOptions, PdfStandard, PdfStandards, Timestamp};
17
18use crate::embedded::EmbeddedTypst;
19use crate::pack::{CompilationDependencySnapshotError, FontCatalogError, PackageTreeError};
20use crate::world::PackWorld;
21use crate::world_trace::{WorldTrace, logical_path};
22use crate::{FontContainerIdentity, Pack, PackageTreeIdentity};
23
24/// The exact embedded Typst compiler implementation that produced a result.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26struct ImplementationIdentity {
27    implementation: &'static str,
28    version: &'static str,
29    source_checksum: &'static str,
30    target: &'static str,
31    target_features: &'static str,
32    feature_set: &'static str,
33    debug_assertions: bool,
34}
35
36impl ImplementationIdentity {
37    const fn new(
38        implementation: &'static str,
39        version: &'static str,
40        source_checksum: &'static str,
41    ) -> Self {
42        Self {
43            implementation,
44            version,
45            source_checksum,
46            target: env!("TYPST_PACK_TARGET"),
47            target_features: env!("TYPST_PACK_CARGO_CFG_TARGET_FEATURE"),
48            feature_set: env!("TYPST_PACK_FEATURE_SET"),
49            debug_assertions: cfg!(debug_assertions),
50        }
51    }
52}
53
54/// The exact embedded Typst compiler implementation that produced a result.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct EngineIdentity(ImplementationIdentity);
57
58impl EngineIdentity {
59    pub(crate) const fn new(
60        implementation: &'static str,
61        version: &'static str,
62        source_checksum: &'static str,
63    ) -> Self {
64        Self(ImplementationIdentity::new(
65            implementation,
66            version,
67            source_checksum,
68        ))
69    }
70
71    /// The compiler crate used for semantic compilation.
72    pub fn implementation(self) -> &'static str {
73        self.0.implementation
74    }
75
76    /// The exact compiler crate version.
77    pub fn version(self) -> &'static str {
78        self.0.version
79    }
80
81    /// The checksum of the exact compiler crate source from the Cargo lockfile.
82    pub fn source_checksum(self) -> &'static str {
83        self.0.source_checksum
84    }
85
86    /// The complete Rust target triple of the compiler implementation.
87    pub fn target(self) -> &'static str {
88        self.0.target
89    }
90
91    /// The target CPU features enabled for the compiler implementation.
92    pub fn target_features(self) -> &'static str {
93        self.0.target_features
94    }
95
96    /// The Cargo feature configuration of the compiler crate.
97    pub fn feature_set(self) -> &'static str {
98        self.0.feature_set
99    }
100
101    /// Whether the compiler implementation was built with debug assertions.
102    pub fn debug_assertions(self) -> bool {
103        self.0.debug_assertions
104    }
105}
106
107/// The exact official exporter implementation that produced a result.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub struct ExporterIdentity(ImplementationIdentity);
110
111impl ExporterIdentity {
112    pub(crate) const fn new(
113        implementation: &'static str,
114        version: &'static str,
115        source_checksum: &'static str,
116    ) -> Self {
117        Self(ImplementationIdentity::new(
118            implementation,
119            version,
120            source_checksum,
121        ))
122    }
123
124    /// The exporter crate used to produce the artifacts.
125    pub fn implementation(self) -> &'static str {
126        self.0.implementation
127    }
128
129    /// The exact exporter crate version.
130    pub fn version(self) -> &'static str {
131        self.0.version
132    }
133
134    /// The checksum of the exact exporter crate source from the Cargo lockfile.
135    pub fn source_checksum(self) -> &'static str {
136        self.0.source_checksum
137    }
138
139    /// The complete Rust target triple of the exporter implementation.
140    pub fn target(self) -> &'static str {
141        self.0.target
142    }
143
144    /// The target CPU features enabled for the exporter implementation.
145    pub fn target_features(self) -> &'static str {
146        self.0.target_features
147    }
148
149    /// The Cargo feature configuration of the exporter crate.
150    pub fn feature_set(self) -> &'static str {
151        self.0.feature_set
152    }
153
154    /// Whether the exporter implementation was built with debug assertions.
155    pub fn debug_assertions(self) -> bool {
156        self.0.debug_assertions
157    }
158}
159
160/// The Document Formats and Page Formats a pack can be compiled to.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162pub enum OutputFormat {
163    Pdf,
164    Png,
165    Svg,
166    /// HTML export is experimental in Typst. Pack-bound compilation derives the
167    /// required [`Feature::Html`](typst::Feature::Html) engine feature.
168    Html,
169}
170
171/// The Typst document model selected for creation or compilation.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
173pub enum TypstTarget {
174    /// A paged document used by PDF and image formats.
175    Paged,
176    /// An HTML document.
177    Html,
178}
179
180impl OutputFormat {
181    /// The conventional file extension for this format.
182    pub fn extension(self) -> &'static str {
183        match self {
184            Self::Pdf => "pdf",
185            Self::Png => "png",
186            Self::Svg => "svg",
187            Self::Html => "html",
188        }
189    }
190}
191
192/// Semantic controls for PDF output.
193#[derive(Debug, Clone)]
194pub struct PdfOutputSpecification {
195    /// Which source pages to export.
196    pub page_selection: PageSelection,
197    /// PDF standards to enforce through the official exporter.
198    pub standards: Vec<PdfStandard>,
199    /// The PDF file identifier, using the official exporter's automatic mode by default.
200    pub identifier: Smart<String>,
201    /// The PDF creator metadata, using the official exporter's automatic mode by default.
202    pub creator: Smart<Option<String>>,
203    /// Whether PDF accessibility tags should be emitted.
204    ///
205    /// Automatic tagging is disabled with a warning for a page subset, matching
206    /// Typst's CLI. Explicit tagging is passed through to the exporter.
207    pub tags: Smart<bool>,
208    /// How the document creation datetime is recorded in PDF metadata.
209    pub creation_timestamp: CreationTimestamp,
210    /// Whether to pretty-print PDF output.
211    pub pretty: bool,
212}
213
214impl Default for PdfOutputSpecification {
215    fn default() -> Self {
216        let pdf = PdfOptions::default();
217        Self {
218            page_selection: PageSelection::default(),
219            standards: vec![],
220            identifier: pdf.ident,
221            creator: pdf.creator,
222            tags: Smart::Auto,
223            creation_timestamp: CreationTimestamp::Automatic,
224            pretty: pdf.pretty,
225        }
226    }
227}
228
229/// Semantic controls for PNG output.
230#[derive(Debug, Clone, Default)]
231pub struct PngOutputSpecification {
232    /// Which source pages to export.
233    pub page_selection: PageSelection,
234    /// Pixels per inch. `None` selects the core default of 144.
235    pub pixels_per_inch: Option<f64>,
236    /// Whether to render into the page bleed region.
237    pub render_bleed: bool,
238}
239
240/// Semantic controls for SVG output.
241#[derive(Debug, Clone, Default)]
242pub struct SvgOutputSpecification {
243    /// Which source pages to export.
244    pub page_selection: PageSelection,
245    /// Whether to render into the page bleed region.
246    pub render_bleed: bool,
247    /// Whether to pretty-print SVG output.
248    pub pretty: bool,
249}
250
251/// Semantic controls for HTML output.
252#[derive(Debug, Clone, Default)]
253pub struct HtmlOutputSpecification {
254    /// Whether to pretty-print HTML output.
255    pub pretty: bool,
256}
257
258/// The required tagged semantic output request.
259#[derive(Debug, Clone)]
260pub enum CompilationOutputSpecification {
261    Pdf(PdfOutputSpecification),
262    Png(PngOutputSpecification),
263    Svg(SvgOutputSpecification),
264    Html(HtmlOutputSpecification),
265}
266
267impl CompilationOutputSpecification {
268    /// The output format represented by this specification.
269    pub fn format(&self) -> OutputFormat {
270        match self {
271            Self::Pdf(_) => OutputFormat::Pdf,
272            Self::Png(_) => OutputFormat::Png,
273            Self::Svg(_) => OutputFormat::Svg,
274            Self::Html(_) => OutputFormat::Html,
275        }
276    }
277
278    fn target(&self) -> TypstTarget {
279        match self {
280            Self::Html(_) => TypstTarget::Html,
281            Self::Pdf(_) | Self::Png(_) | Self::Svg(_) => TypstTarget::Paged,
282        }
283    }
284}
285
286/// How an effective compilation request value was established.
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288pub enum RequestValueOrigin {
289    /// The library caller supplied the value directly.
290    CallerSupplied,
291    /// The semantic core supplied its deterministic default.
292    CoreDefaulted,
293    /// The semantic core derived the value from another request value.
294    CoreDerived,
295    /// An adapter resolved an ambient or adapter-level default before execution.
296    AdapterResolved,
297}
298
299/// The exact or explicitly absent time used by Typst document-time requests.
300#[derive(Debug, Clone, Copy, PartialEq, Hash)]
301pub enum DocumentTime {
302    /// Document-time requests have no value.
303    Absent,
304    /// Return one fixed Typst date or datetime.
305    Fixed(Datetime),
306    /// Resolve one fixed UTC instant under each requested timezone offset.
307    UnixTimestamp(i64),
308}
309
310impl DocumentTime {
311    fn identity_projection(self) -> (Option<Datetime>, Option<i64>) {
312        match self {
313            Self::Absent => (None, None),
314            Self::Fixed(datetime) => (Some(datetime), None),
315            Self::UnixTimestamp(timestamp) => (None, Some(timestamp)),
316        }
317    }
318}
319
320/// One effective request value together with its provenance.
321#[derive(Debug, Clone)]
322pub struct EffectiveRequestValue<T> {
323    value: T,
324    origin: RequestValueOrigin,
325}
326
327impl<T> EffectiveRequestValue<T> {
328    fn new(value: T, origin: RequestValueOrigin) -> Self {
329        Self { value, origin }
330    }
331
332    /// The exact effective value.
333    pub fn value(&self) -> &T {
334        &self.value
335    }
336
337    /// How the effective value was established.
338    pub fn origin(&self) -> RequestValueOrigin {
339        self.origin
340    }
341}
342
343/// One enabled Typst engine feature and why it is enabled.
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
345pub struct EffectiveEngineFeature {
346    value: Feature,
347    origin: RequestValueOrigin,
348}
349
350impl EffectiveEngineFeature {
351    /// The exact feature from the pinned Typst feature set.
352    pub fn value(self) -> Feature {
353        self.value
354    }
355
356    /// How the feature became enabled.
357    pub fn origin(self) -> RequestValueOrigin {
358        self.origin
359    }
360}
361
362/// Every effective shared semantic value passed to the embedded Typst engine.
363#[derive(Debug, Clone)]
364pub struct CompilationRequestInventory {
365    output_specification: EffectiveRequestValue<CompilationOutputSpecification>,
366    output_origins: CompilationOutputOrigins,
367    inputs: EffectiveRequestValue<TypstInputsInventory>,
368    overrides: EffectiveRequestValue<PackOverridesInventory>,
369    selected_features: Vec<EffectiveEngineFeature>,
370    features: Vec<EffectiveEngineFeature>,
371    document_time: EffectiveRequestValue<DocumentTime>,
372}
373
374impl CompilationRequestInventory {
375    /// The tagged output controls, including their deterministic defaults.
376    pub fn output_specification(&self) -> &EffectiveRequestValue<CompilationOutputSpecification> {
377        &self.output_specification
378    }
379
380    /// Safe evidence for the exact `sys.inputs` dictionary.
381    pub fn inputs(&self) -> &EffectiveRequestValue<TypstInputsInventory> {
382        &self.inputs
383    }
384
385    /// Safe evidence for the exact Pack Override Set.
386    pub fn overrides(&self) -> &EffectiveRequestValue<PackOverridesInventory> {
387        &self.overrides
388    }
389
390    /// Format-specific provenance for effective output controls.
391    pub fn output_origins(&self) -> CompilationOutputOrigins {
392        self.output_origins
393    }
394
395    /// The exact effective pinned Typst feature set.
396    pub fn features(&self) -> &[EffectiveEngineFeature] {
397        &self.features
398    }
399
400    /// Features explicitly selected by a caller or adapter before derivation.
401    pub fn selected_features(&self) -> &[EffectiveEngineFeature] {
402        &self.selected_features
403    }
404
405    /// The exact Compilation Document Time, including explicit absence.
406    pub fn document_time(&self) -> &EffectiveRequestValue<DocumentTime> {
407        &self.document_time
408    }
409}
410
411/// Format-specific provenance for output controls resolved during preparation.
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413pub enum CompilationOutputOrigins {
414    Pdf {
415        tags: RequestValueOrigin,
416        creation_time: RequestValueOrigin,
417    },
418    Png {
419        pixels_per_inch: RequestValueOrigin,
420    },
421    Svg,
422    Html,
423}
424
425/// Safe, role-bound evidence for potentially sensitive Typst inputs.
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
427pub struct TypstInputsInventory {
428    commitment: u128,
429    entry_count: usize,
430    total_key_bytes: usize,
431    total_value_repr_bytes: usize,
432}
433
434impl TypstInputsInventory {
435    /// The commitment schema, including the pinned value-hash implementation.
436    pub fn schema(self) -> &'static str {
437        "typst-pack-inputs-v1+typst-0.15"
438    }
439
440    /// The role-bound commitment digest in big-endian order.
441    pub fn commitment(self) -> [u8; 16] {
442        self.commitment.to_be_bytes()
443    }
444
445    /// The number of input keys represented by the commitment.
446    pub fn entry_count(self) -> usize {
447        self.entry_count
448    }
449
450    /// The exact total UTF-8 byte length of all input keys.
451    pub fn total_key_bytes(self) -> usize {
452        self.total_key_bytes
453    }
454
455    /// The exact total UTF-8 byte length of the pinned Typst representations.
456    pub fn total_value_repr_bytes(self) -> usize {
457        self.total_value_repr_bytes
458    }
459}
460
461/// Safe, role-bound evidence for one replacement in a Pack Override Set.
462#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct PackOverrideInventoryEntry {
464    path: String,
465    byte_len: usize,
466    commitment: u128,
467}
468
469impl PackOverrideInventoryEntry {
470    /// The commitment schema, including the pinned value-hash implementation.
471    pub fn schema(&self) -> &'static str {
472        "typst-pack-override-v1+typst-0.15"
473    }
474
475    /// The canonical contained project path being replaced.
476    pub fn path(&self) -> &str {
477        &self.path
478    }
479
480    /// The exact replacement byte length.
481    pub fn byte_len(&self) -> usize {
482        self.byte_len
483    }
484
485    /// The role-bound commitment digest in big-endian order.
486    pub fn commitment(&self) -> [u8; 16] {
487        self.commitment.to_be_bytes()
488    }
489}
490
491/// Safe evidence for an immutable Pack Override Set.
492#[derive(Debug, Clone, Default, PartialEq, Eq)]
493pub struct PackOverridesInventory(Vec<PackOverrideInventoryEntry>);
494
495impl PackOverridesInventory {
496    pub fn len(&self) -> usize {
497        self.0.len()
498    }
499
500    pub fn is_empty(&self) -> bool {
501        self.0.is_empty()
502    }
503
504    pub fn iter(&self) -> impl Iterator<Item = &PackOverrideInventoryEntry> {
505        self.0.iter()
506    }
507}
508
509/// An immutable set of contained project-file replacements bound to one Pack.
510#[derive(Debug, Clone)]
511pub struct PackOverrideSet {
512    pack_identity: crate::PackIdentity,
513    project_paths: BTreeSet<String>,
514    replacements: BTreeMap<String, Bytes>,
515}
516
517impl PackOverrideSet {
518    /// Starts an empty override set bound to `pack`.
519    pub fn new(pack: &Pack) -> Self {
520        Self {
521            pack_identity: pack.identity(),
522            project_paths: pack.files().map(|(path, _)| path.to_owned()).collect(),
523            replacements: BTreeMap::new(),
524        }
525    }
526
527    /// Adds one replacement after strict Pack-owned preflight.
528    pub fn replace(
529        mut self,
530        path: impl AsRef<str>,
531        data: impl Into<Vec<u8>>,
532    ) -> Result<Self, PackOverrideSetError> {
533        let supplied = path.as_ref();
534        let path = Pack::canonical_project_path(supplied).map_err(|message| {
535            PackOverrideSetError::InvalidProjectPath {
536                path: supplied.to_owned(),
537                message,
538            }
539        })?;
540        if self.replacements.contains_key(&path) {
541            return Err(PackOverrideSetError::DuplicateProjectPath { path });
542        }
543        if !self.project_paths.contains(&path) {
544            return Err(PackOverrideSetError::MissingProjectPath { path });
545        }
546        self.replacements.insert(path, Bytes::new(data.into()));
547        Ok(self)
548    }
549}
550
551/// A Pack-owned Pack Override preflight rejection.
552#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
553pub enum PackOverrideSetError {
554    #[error("invalid Pack Override project path `{path}`: {message}")]
555    InvalidProjectPath { path: String, message: String },
556    #[error("Pack Override path `{path}` is declared more than once")]
557    DuplicateProjectPath { path: String },
558    #[error("Pack Override path `{path}` is not a contained project file")]
559    MissingProjectPath { path: String },
560}
561
562/// The pre-execution identity of a fully specified semantic compilation.
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
564pub struct CompilationIdentity(u128);
565
566impl CompilationIdentity {
567    /// The canonical identity kind.
568    pub fn kind(self) -> &'static str {
569        "compilation"
570    }
571
572    /// The identity schema used by this release.
573    pub fn schema(self) -> &'static str {
574        "typst-pack-compilation-v1"
575    }
576
577    /// The deterministic digest algorithm used by this identity schema.
578    pub fn algorithm(self) -> &'static str {
579        "typst-hash128-0.15"
580    }
581
582    /// The deterministic 128-bit identity digest in big-endian order.
583    pub fn digest(self) -> [u8; 16] {
584        self.0.to_be_bytes()
585    }
586}
587
588/// An explicit semantic compilation request bound to one validated [`Pack`].
589///
590/// Compilation through this request has no project, package, font, clock,
591/// environment, cache, or network fallback beyond the Pack and these values.
592pub struct PackCompilationRequest {
593    pack: Pack,
594    output_specification: EffectiveRequestValue<CompilationOutputSpecification>,
595    inputs: EffectiveRequestValue<Dict>,
596    overrides: EffectiveRequestValue<PackOverrideSet>,
597    features: Vec<EffectiveEngineFeature>,
598    document_time: EffectiveRequestValue<DocumentTime>,
599    package_fulfillments: BTreeMap<String, PackageTreeFulfillment>,
600    font_fulfillments: BTreeMap<FontContainerIdentity, FontContainerFulfillment>,
601}
602
603/// One externally acquired Complete Package Tree and operational metadata.
604#[derive(Debug, Clone)]
605pub struct PackageTreeFulfillment {
606    files: Vec<(String, Bytes)>,
607    provenance: Option<String>,
608    cache_hit: bool,
609}
610
611impl PackageTreeFulfillment {
612    pub fn new<I, P, D>(files: I) -> Self
613    where
614        I: IntoIterator<Item = (P, D)>,
615        P: Into<String>,
616        D: Into<Vec<u8>>,
617    {
618        Self {
619            files: files
620                .into_iter()
621                .map(|(path, data)| (path.into(), Bytes::new(data.into())))
622                .collect(),
623            provenance: None,
624            cache_hit: false,
625        }
626    }
627
628    pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
629        self.provenance = Some(provenance.into());
630        self
631    }
632
633    pub fn cache_hit(mut self, cache_hit: bool) -> Self {
634        self.cache_hit = cache_hit;
635        self
636    }
637}
638
639/// Exact externally supplied Font Container bytes and non-semantic metadata.
640#[derive(Debug, Clone)]
641pub struct FontContainerFulfillment {
642    data: Bytes,
643    provenance: Option<String>,
644    licensing: Option<String>,
645}
646
647impl FontContainerFulfillment {
648    pub fn new(data: impl Into<Vec<u8>>) -> Self {
649        Self {
650            data: Bytes::new(data.into()),
651            provenance: None,
652            licensing: None,
653        }
654    }
655
656    pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
657        self.provenance = Some(provenance.into());
658        self
659    }
660
661    pub fn licensing(mut self, licensing: impl Into<String>) -> Self {
662        self.licensing = Some(licensing.into());
663        self
664    }
665}
666
667impl PackCompilationRequest {
668    /// Binds a validated Pack to a tagged semantic output specification.
669    pub fn new(pack: Pack, output_specification: CompilationOutputSpecification) -> Self {
670        let overrides = PackOverrideSet::new(&pack);
671        Self {
672            pack,
673            output_specification: EffectiveRequestValue::new(
674                output_specification,
675                RequestValueOrigin::CallerSupplied,
676            ),
677            inputs: EffectiveRequestValue::new(Dict::new(), RequestValueOrigin::CoreDefaulted),
678            overrides: EffectiveRequestValue::new(overrides, RequestValueOrigin::CoreDefaulted),
679            features: Vec::new(),
680            document_time: EffectiveRequestValue::new(
681                DocumentTime::Absent,
682                RequestValueOrigin::CoreDefaulted,
683            ),
684            package_fulfillments: BTreeMap::new(),
685            font_fulfillments: BTreeMap::new(),
686        }
687    }
688
689    #[cfg(feature = "diagnostics")]
690    #[doc(hidden)]
691    pub fn adapter_resolved_output(mut self) -> Self {
692        self.output_specification.origin = RequestValueOrigin::AdapterResolved;
693        self
694    }
695
696    /// Sets the exact values exposed to document code as `sys.inputs`.
697    pub fn inputs(mut self, inputs: Dict) -> Self {
698        self.inputs = EffectiveRequestValue::new(inputs, RequestValueOrigin::CallerSupplied);
699        self
700    }
701
702    /// Sets `sys.inputs` after an adapter has resolved its external defaults.
703    pub fn adapter_resolved_inputs(mut self, inputs: Dict) -> Self {
704        self.inputs = EffectiveRequestValue::new(inputs, RequestValueOrigin::AdapterResolved);
705        self
706    }
707
708    /// Applies one immutable Pack-bound Pack Override Set.
709    pub fn overrides(mut self, overrides: PackOverrideSet) -> Self {
710        self.overrides = EffectiveRequestValue::new(overrides, RequestValueOrigin::CallerSupplied);
711        self
712    }
713
714    /// Enables one official Typst engine feature.
715    pub fn feature(mut self, feature: Feature) -> Self {
716        self.features.push(EffectiveEngineFeature {
717            value: feature,
718            origin: RequestValueOrigin::CallerSupplied,
719        });
720        self
721    }
722
723    /// Enables a feature selected by an adapter before semantic preparation.
724    pub fn adapter_resolved_feature(mut self, feature: Feature) -> Self {
725        self.features.push(EffectiveEngineFeature {
726            value: feature,
727            origin: RequestValueOrigin::AdapterResolved,
728        });
729        self
730    }
731
732    /// Sets the exact value returned by document-time requests.
733    pub fn document_time(mut self, document_time: DocumentTime) -> Self {
734        self.document_time =
735            EffectiveRequestValue::new(document_time, RequestValueOrigin::CallerSupplied);
736        self
737    }
738
739    /// Sets the exact document time resolved by an adapter.
740    pub fn adapter_resolved_document_time(mut self, document_time: DocumentTime) -> Self {
741        self.document_time =
742            EffectiveRequestValue::new(document_time, RequestValueOrigin::AdapterResolved);
743        self
744    }
745
746    /// Supplies bytes for one exact external Font Container requirement.
747    pub fn font_fulfillment(
748        mut self,
749        expected: FontContainerIdentity,
750        fulfillment: FontContainerFulfillment,
751    ) -> Self {
752        self.font_fulfillments.insert(expected, fulfillment);
753        self
754    }
755
756    /// Supplies one Complete Package Tree under its exact Typst package specification.
757    pub fn package_fulfillment(
758        mut self,
759        spec: PackageSpec,
760        fulfillment: PackageTreeFulfillment,
761    ) -> Self {
762        self.package_fulfillments
763            .insert(spec.to_string(), fulfillment);
764        self
765    }
766}
767
768/// The source of the document creation datetime recorded in PDF metadata.
769#[derive(Debug, Clone, Copy, Default, Hash)]
770pub enum CreationTimestamp {
771    /// Derive the timestamp from the world's `today`.
772    #[default]
773    Automatic,
774    /// Record an explicit UTC timestamp.
775    Explicit(Timestamp),
776    /// Omit creation datetime metadata without falling back to the world.
777    Omit,
778}
779
780/// A one-indexed, inclusive page range with optional open ends.
781pub type PageRange = std::ops::RangeInclusive<Option<NonZeroUsize>>;
782
783/// A selection of one-indexed source page ranges.
784///
785/// An empty range collection selects all source pages. Ranges are inclusive
786/// and may have open ends.
787#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
788pub struct PageSelection {
789    ranges: Vec<PageRange>,
790}
791
792impl PageSelection {
793    /// Selects all source pages.
794    pub fn all() -> Self {
795        Self::default()
796    }
797
798    /// Selects the union of the given source page ranges.
799    ///
800    /// An empty collection selects all source pages.
801    pub fn new(ranges: Vec<PageRange>) -> Self {
802        Self { ranges }
803    }
804
805    /// The selected source page ranges.
806    pub fn ranges(&self) -> &[PageRange] {
807        &self.ranges
808    }
809
810    fn typst_page_ranges(&self) -> Option<typst::layout::PageRanges> {
811        (!self.ranges.is_empty()).then(|| typst::layout::PageRanges::new(self.ranges.clone()))
812    }
813}
814
815/// Parses a textual page selection like `1,3-5,9-`.
816pub fn parse_page_selection(text: &str) -> Result<PageSelection, String> {
817    text.split(',')
818        .map(|part| {
819            let part = part.trim();
820            let parse = |value: &str| -> Result<NonZeroUsize, String> {
821                if value == "0" {
822                    Err("page numbers start at one".to_owned())
823                } else {
824                    value
825                        .parse::<NonZeroUsize>()
826                        .map_err(|_| format!("`{value}` is not a valid page number"))
827                }
828            };
829            match part
830                .split('-')
831                .map(str::trim)
832                .collect::<Vec<_>>()
833                .as_slice()
834            {
835                [] | [""] => Err("page export range must not be empty".to_owned()),
836                [single] => {
837                    let page = parse(single)?;
838                    Ok(Some(page)..=Some(page))
839                }
840                ["", ""] => Err("page export range must have start or end".to_owned()),
841                [start, ""] => Ok(Some(parse(start)?)..=None),
842                ["", end] => Ok(None..=Some(parse(end)?)),
843                [start, end] => {
844                    let start = parse(start)?;
845                    let end = parse(end)?;
846                    if start > end {
847                        Err("page export range must end at a page after the start".to_owned())
848                    } else {
849                        Ok(Some(start)..=Some(end))
850                    }
851                }
852                _ => Err("page export range must have a single hyphen".to_owned()),
853            }
854        })
855        .collect::<Result<Vec<_>, _>>()
856        .map(PageSelection::new)
857}
858
859/// One file produced by compiling a pack.
860#[derive(Debug, Clone)]
861pub struct CompilationArtifact {
862    format: OutputFormat,
863    bytes: Vec<u8>,
864    source_page_number: Option<NonZeroUsize>,
865}
866
867/// Whether the official compiler and exporter accepted the compilation.
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
869pub enum CompilationStatus {
870    Succeeded,
871    Rejected,
872}
873
874/// The official phase that emitted a diagnostic.
875#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
876pub enum DiagnosticPhase {
877    Compilation,
878    Export,
879}
880
881/// The exact embedded implementation that emitted a diagnostic.
882#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
883pub enum DiagnosticProducer {
884    Engine(EngineIdentity),
885    Exporter(ExporterIdentity),
886}
887
888/// Official Typst diagnostic severity.
889#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
890pub enum DiagnosticSeverity {
891    Error,
892    Warning,
893}
894
895/// A source location expressed in the Pack's logical namespace.
896#[derive(Debug, Clone, PartialEq, Eq, Hash)]
897pub struct LogicalSpan {
898    logical_path: Option<String>,
899    byte_range: Option<Range<usize>>,
900}
901
902impl LogicalSpan {
903    /// The logical project or package path, independent of transport location.
904    pub fn logical_path(&self) -> Option<&str> {
905        self.logical_path.as_deref()
906    }
907
908    /// The exact source byte range when Typst attached one.
909    pub fn byte_range(&self) -> Option<&Range<usize>> {
910        self.byte_range.as_ref()
911    }
912}
913
914/// A structured hint attached to an official diagnostic.
915#[derive(Debug, Clone, PartialEq, Eq, Hash)]
916pub struct DiagnosticHint {
917    message: String,
918    span: LogicalSpan,
919}
920
921impl DiagnosticHint {
922    pub fn message(&self) -> &str {
923        &self.message
924    }
925
926    pub fn span(&self) -> &LogicalSpan {
927        &self.span
928    }
929}
930
931/// The kind of one official diagnostic tracepoint.
932#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
933pub enum TracepointKind {
934    Call,
935    Show,
936    Import,
937    Include,
938}
939
940/// One structured tracepoint attached to an official diagnostic.
941#[derive(Debug, Clone, PartialEq, Eq, Hash)]
942pub struct DiagnosticTracepoint {
943    kind: TracepointKind,
944    value: Option<String>,
945    span: LogicalSpan,
946}
947
948impl DiagnosticTracepoint {
949    pub fn kind(&self) -> TracepointKind {
950        self.kind
951    }
952
953    pub fn value(&self) -> Option<&str> {
954        self.value.as_deref()
955    }
956
957    pub fn span(&self) -> &LogicalSpan {
958        &self.span
959    }
960}
961
962/// A structured compiler or exporter diagnostic.
963#[derive(Debug, Clone, PartialEq, Eq, Hash)]
964pub struct CompilationDiagnostic {
965    severity: DiagnosticSeverity,
966    message: String,
967    span: LogicalSpan,
968    hints: Vec<DiagnosticHint>,
969    trace: Vec<DiagnosticTracepoint>,
970    phase: DiagnosticPhase,
971    producer: DiagnosticProducer,
972    source_page_number: Option<NonZeroUsize>,
973}
974
975impl CompilationDiagnostic {
976    pub fn severity(&self) -> DiagnosticSeverity {
977        self.severity
978    }
979
980    pub fn message(&self) -> &str {
981        &self.message
982    }
983
984    pub fn span(&self) -> &LogicalSpan {
985        &self.span
986    }
987
988    pub fn hints(&self) -> &[DiagnosticHint] {
989        &self.hints
990    }
991
992    pub fn trace(&self) -> &[DiagnosticTracepoint] {
993        &self.trace
994    }
995
996    pub fn phase(&self) -> DiagnosticPhase {
997        self.phase
998    }
999
1000    pub fn producer(&self) -> DiagnosticProducer {
1001        self.producer
1002    }
1003
1004    /// The Source Page Number whose Page Format export failed, when known.
1005    pub fn source_page_number(&self) -> Option<NonZeroUsize> {
1006        self.source_page_number
1007    }
1008}
1009
1010/// The stable document facts reached before complete export.
1011#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1012pub struct CompilationDocumentSummary {
1013    target: TypstTarget,
1014    source_page_count: Option<usize>,
1015}
1016
1017impl CompilationDocumentSummary {
1018    pub fn target(self) -> TypstTarget {
1019        self.target
1020    }
1021
1022    pub fn source_page_count(self) -> Option<usize> {
1023        self.source_page_count
1024    }
1025}
1026
1027/// The kind of dependency request made by the embedded engine.
1028#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1029pub enum CompilationAccessKind {
1030    Source,
1031    File,
1032    Font,
1033}
1034
1035/// The stable outcome of one dependency request.
1036#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1037pub enum CompilationAccessOutcome {
1038    Read {
1039        byte_length: usize,
1040        digest: [u8; 16],
1041    },
1042    Missing,
1043    Failed,
1044}
1045
1046/// One canonical dependency observation made by the embedded engine.
1047#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1048pub struct CompilationAccessObservation {
1049    kind: CompilationAccessKind,
1050    logical_path: String,
1051    font_index: Option<usize>,
1052    outcome: CompilationAccessOutcome,
1053}
1054
1055impl CompilationAccessObservation {
1056    pub(crate) fn new(
1057        kind: CompilationAccessKind,
1058        logical_path: String,
1059        font_index: Option<usize>,
1060        outcome: CompilationAccessOutcome,
1061    ) -> Self {
1062        Self {
1063            kind,
1064            logical_path,
1065            font_index,
1066            outcome,
1067        }
1068    }
1069
1070    pub fn kind(&self) -> CompilationAccessKind {
1071        self.kind
1072    }
1073
1074    pub fn logical_path(&self) -> &str {
1075        &self.logical_path
1076    }
1077
1078    pub fn font_index(&self) -> Option<usize> {
1079        self.font_index
1080    }
1081
1082    pub fn outcome(&self) -> &CompilationAccessOutcome {
1083        &self.outcome
1084    }
1085}
1086
1087/// Canonical accesses retained by a semantic compilation result.
1088#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
1089pub struct CompilationAccessTrace {
1090    observations: BTreeSet<CompilationAccessObservation>,
1091}
1092
1093impl CompilationAccessTrace {
1094    pub fn observations(&self) -> impl Iterator<Item = &CompilationAccessObservation> {
1095        self.observations.iter()
1096    }
1097
1098    pub(crate) fn from_observations(observations: BTreeSet<CompilationAccessObservation>) -> Self {
1099        Self { observations }
1100    }
1101}
1102
1103/// The identity of one complete compiler and exporter result.
1104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1105pub struct CompilationResultIdentity(u128);
1106
1107impl CompilationResultIdentity {
1108    pub fn kind(self) -> &'static str {
1109        "compilation-result"
1110    }
1111
1112    pub fn schema(self) -> &'static str {
1113        "typst-pack-compilation-result-v1"
1114    }
1115
1116    pub fn algorithm(self) -> &'static str {
1117        "typst-hash128-0.15"
1118    }
1119
1120    pub fn digest(self) -> [u8; 16] {
1121        self.0.to_be_bytes()
1122    }
1123}
1124
1125/// Operational evidence retained for one exact package fulfillment.
1126#[derive(Debug, Clone, PartialEq, Eq)]
1127pub struct PackageFulfillmentReport {
1128    spec: PackageSpec,
1129    tree_identity: PackageTreeIdentity,
1130    embedded: bool,
1131    provenance: Option<String>,
1132    cache_hit: bool,
1133}
1134
1135impl PackageFulfillmentReport {
1136    pub fn spec(&self) -> &PackageSpec {
1137        &self.spec
1138    }
1139    pub fn tree_identity(&self) -> PackageTreeIdentity {
1140        self.tree_identity
1141    }
1142    pub fn embedded(&self) -> bool {
1143        self.embedded
1144    }
1145    pub fn provenance(&self) -> Option<&str> {
1146        self.provenance.as_deref()
1147    }
1148    pub fn cache_hit(&self) -> bool {
1149        self.cache_hit
1150    }
1151}
1152
1153/// Operational evidence retained for one exact font fulfillment.
1154#[derive(Debug, Clone, PartialEq, Eq)]
1155pub struct FontFulfillmentReport {
1156    container_identity: FontContainerIdentity,
1157    embedded: bool,
1158    provenance: Option<String>,
1159    licensing: Option<String>,
1160}
1161
1162impl FontFulfillmentReport {
1163    pub fn container_identity(&self) -> FontContainerIdentity {
1164        self.container_identity
1165    }
1166    pub fn embedded(&self) -> bool {
1167        self.embedded
1168    }
1169    pub fn provenance(&self) -> Option<&str> {
1170        self.provenance.as_deref()
1171    }
1172    pub fn licensing(&self) -> Option<&str> {
1173        self.licensing.as_deref()
1174    }
1175}
1176
1177/// Operational dependency evidence surrounding one official semantic result.
1178#[derive(Debug, Clone, Default, PartialEq, Eq)]
1179pub struct CompilationFulfillmentReport {
1180    packages: Vec<PackageFulfillmentReport>,
1181    fonts: Vec<FontFulfillmentReport>,
1182}
1183
1184impl CompilationFulfillmentReport {
1185    pub fn packages(&self) -> &[PackageFulfillmentReport] {
1186        &self.packages
1187    }
1188    pub fn fonts(&self) -> &[FontFulfillmentReport] {
1189        &self.fonts
1190    }
1191}
1192
1193/// The immutable account of an accepted compilation through complete export.
1194#[derive(Debug, Clone)]
1195pub struct CompilationReport {
1196    outcome: CompilationReportOutcome,
1197    fulfillments: CompilationFulfillmentReport,
1198}
1199
1200#[derive(Debug, Clone)]
1201pub enum CompilationReportOutcome {
1202    Result(Box<CompilationResult>),
1203    Operation {
1204        outcome: CompilationOperationOutcome,
1205        request_inventory: Box<CompilationRequestInventory>,
1206        compilation_identity: CompilationIdentity,
1207    },
1208}
1209
1210impl CompilationReport {
1211    pub fn outcome(&self) -> &CompilationReportOutcome {
1212        &self.outcome
1213    }
1214
1215    pub fn result(&self) -> Option<&CompilationResult> {
1216        match &self.outcome {
1217            CompilationReportOutcome::Result(result) => Some(result.as_ref()),
1218            CompilationReportOutcome::Operation { .. } => None,
1219        }
1220    }
1221    pub fn fulfillments(&self) -> &CompilationFulfillmentReport {
1222        &self.fulfillments
1223    }
1224}
1225
1226/// The semantic result of an accepted Pack compilation request.
1227#[derive(Debug, Clone)]
1228pub struct CompilationResult {
1229    status: CompilationStatus,
1230    artifacts: Vec<CompilationArtifact>,
1231    diagnostics: Vec<CompilationDiagnostic>,
1232    pack_warnings: Vec<PackCompilationWarning>,
1233    document: CompilationDocumentSummary,
1234    access_trace: CompilationAccessTrace,
1235    result_identity: CompilationResultIdentity,
1236    request_inventory: CompilationRequestInventory,
1237    compilation_identity: CompilationIdentity,
1238    engine_identity: EngineIdentity,
1239    exporter_identity: ExporterIdentity,
1240}
1241
1242impl CompilationResult {
1243    pub fn status(&self) -> CompilationStatus {
1244        self.status
1245    }
1246
1247    pub fn artifacts(&self) -> &[CompilationArtifact] {
1248        &self.artifacts
1249    }
1250
1251    pub fn diagnostics(&self) -> &[CompilationDiagnostic] {
1252        &self.diagnostics
1253    }
1254
1255    /// Pack-owned warnings kept separate from official diagnostics.
1256    pub fn pack_warnings(&self) -> &[PackCompilationWarning] {
1257        &self.pack_warnings
1258    }
1259
1260    pub fn source_page_count(&self) -> Option<usize> {
1261        self.document.source_page_count
1262    }
1263
1264    pub fn document(&self) -> CompilationDocumentSummary {
1265        self.document
1266    }
1267
1268    pub fn access_trace(&self) -> &CompilationAccessTrace {
1269        &self.access_trace
1270    }
1271
1272    pub fn result_identity(&self) -> CompilationResultIdentity {
1273        self.result_identity
1274    }
1275
1276    /// The complete effective semantic request prepared before execution.
1277    pub fn request_inventory(&self) -> &CompilationRequestInventory {
1278        &self.request_inventory
1279    }
1280
1281    /// The identity of the complete prepared request and implementation.
1282    pub fn compilation_identity(&self) -> CompilationIdentity {
1283        self.compilation_identity
1284    }
1285
1286    pub fn engine_identity(&self) -> EngineIdentity {
1287        self.engine_identity
1288    }
1289
1290    pub fn exporter_identity(&self) -> ExporterIdentity {
1291        self.exporter_identity
1292    }
1293}
1294
1295/// A Pack-owned semantic request warning.
1296#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1297pub struct PackCompilationWarning {
1298    message: String,
1299    hints: Vec<String>,
1300}
1301
1302impl PackCompilationWarning {
1303    pub fn message(&self) -> &str {
1304        &self.message
1305    }
1306
1307    pub fn hints(&self) -> &[String] {
1308        &self.hints
1309    }
1310}
1311
1312impl CompilationArtifact {
1313    /// The format of this artifact.
1314    pub fn format(&self) -> OutputFormat {
1315        self.format
1316    }
1317
1318    /// The one-based physical source page for a Page Format artifact.
1319    ///
1320    /// Document Format artifacts have no single Source Page Number.
1321    pub fn source_page_number(&self) -> Option<NonZeroUsize> {
1322        self.source_page_number
1323    }
1324
1325    /// Borrows the artifact bytes.
1326    pub fn bytes(&self) -> &[u8] {
1327        &self.bytes
1328    }
1329
1330    /// Extracts the owned artifact bytes.
1331    pub fn into_bytes(self) -> Vec<u8> {
1332        self.bytes
1333    }
1334}
1335
1336/// The result of compiling a pack.
1337#[derive(Debug, Clone)]
1338pub(crate) struct CompilationOutput {
1339    /// The produced Compilation Output Artifacts.
1340    pub artifacts: Vec<CompilationArtifact>,
1341    /// Warnings emitted during compilation.
1342    pub warnings: EcoVec<SourceDiagnostic>,
1343    pack_warnings: EcoVec<SourceDiagnostic>,
1344    source_page_count: Option<usize>,
1345}
1346
1347/// A failed compilation.
1348#[derive(Debug, thiserror::Error)]
1349pub(crate) enum CompileError {
1350    /// The official PDF standards validator rejected the requested set.
1351    #[error(transparent)]
1352    InvalidPdfStandards(#[from] PdfStandardsValidationError),
1353    /// Compilation or export produced errors; warnings are included for
1354    /// complete reporting.
1355    #[error("compilation failed with {} error(s)", errors.len())]
1356    Diagnostics {
1357        errors: EcoVec<SourceDiagnostic>,
1358        warnings: EcoVec<SourceDiagnostic>,
1359        pack_warnings: EcoVec<SourceDiagnostic>,
1360        phase: DiagnosticPhase,
1361        source_page_count: Option<usize>,
1362    },
1363    /// PNG export failed after compilation completed.
1364    #[error("PNG export failed for source page {source_page_number}: {message}")]
1365    PngExport {
1366        message: String,
1367        /// Warnings emitted before PNG export failed.
1368        warnings: EcoVec<SourceDiagnostic>,
1369        pack_warnings: EcoVec<SourceDiagnostic>,
1370        source_page_count: usize,
1371        /// The page whose PNG encoding failed.
1372        source_page_number: NonZeroUsize,
1373    },
1374}
1375
1376/// A lossless projection of an official PDF standards validation error.
1377#[derive(Debug, thiserror::Error)]
1378#[error("invalid PDF standards: {message}")]
1379pub struct PdfStandardsValidationError {
1380    message: String,
1381    hints: Vec<String>,
1382}
1383
1384impl PdfStandardsValidationError {
1385    /// The official validation message.
1386    pub fn message(&self) -> &str {
1387        &self.message
1388    }
1389
1390    /// The official validation hints in their original order.
1391    pub fn hints(&self) -> &[String] {
1392        &self.hints
1393    }
1394
1395    /// Consumes the error into its official message and ordered hints.
1396    pub fn into_parts(self) -> (String, Vec<String>) {
1397        (self.message, self.hints)
1398    }
1399}
1400
1401/// One independently detectable issue in a rejected semantic request.
1402#[derive(Debug, thiserror::Error)]
1403pub enum CompilationRequestIssue {
1404    /// The Pack compilation contract intentionally excludes Typst Bundle.
1405    #[error("the Typst Bundle feature is not supported for Pack compilation")]
1406    UnsupportedBundleFeature,
1407    /// PNG resolution must be finite and greater than zero.
1408    #[error("PNG pixels per inch must be finite and greater than zero")]
1409    InvalidPpi,
1410    /// The official PDF standards validator rejected the requested set.
1411    #[error(transparent)]
1412    InvalidPdfStandards(PdfStandardsValidationError),
1413    /// The Pack Override Set was preflighted against a different Pack.
1414    #[error("the Pack Override Set is bound to a different Pack")]
1415    OverrideSetPackMismatch,
1416    /// The document-time Unix timestamp cannot be represented.
1417    #[error("the document-time UNIX timestamp is out of range")]
1418    InvalidDocumentTimestamp,
1419}
1420
1421/// A rejected semantic request and its complete effective inventory.
1422#[derive(Debug)]
1423pub struct CompilationRequestRejection {
1424    issues: Vec<CompilationRequestIssue>,
1425    request_inventory: Box<CompilationRequestInventory>,
1426}
1427
1428impl CompilationRequestRejection {
1429    /// The independently detectable request issues in stable order.
1430    pub fn issues(&self) -> &[CompilationRequestIssue] {
1431        &self.issues
1432    }
1433
1434    /// Every effective semantic request value retained before rejection.
1435    pub fn request_inventory(&self) -> &CompilationRequestInventory {
1436        &self.request_inventory
1437    }
1438
1439    fn new(
1440        issues: Vec<CompilationRequestIssue>,
1441        request_inventory: CompilationRequestInventory,
1442    ) -> Self {
1443        debug_assert!(!issues.is_empty());
1444        Self {
1445            issues,
1446            request_inventory: Box::new(request_inventory),
1447        }
1448    }
1449}
1450
1451impl std::fmt::Display for CompilationRequestRejection {
1452    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1453        if let [issue] = self.issues.as_slice() {
1454            issue.fmt(formatter)
1455        } else {
1456            formatter.write_str("the compilation request contains multiple invalid values")
1457        }
1458    }
1459}
1460
1461impl std::error::Error for CompilationRequestRejection {}
1462
1463/// A Pack-owned operational outcome before official compilation begins.
1464#[derive(Debug, Clone, thiserror::Error)]
1465pub enum CompilationOperationOutcome {
1466    /// The request supplied no authority for declared external packages.
1467    #[error("external package fulfillment is unavailable for {packages:?}")]
1468    MissingExternalPackageFulfillment { packages: Vec<PackageSpec> },
1469    /// Supplied files do not match the exact required Complete Package Tree.
1470    #[error("external package fulfillment for {spec} supplied {actual:?}, expected {expected:?}")]
1471    MismatchedExternalPackageTree {
1472        spec: PackageSpec,
1473        expected: PackageTreeIdentity,
1474        actual: PackageTreeIdentity,
1475        expected_file_count: u64,
1476        actual_file_count: u64,
1477        expected_byte_length: u64,
1478        actual_byte_length: u64,
1479    },
1480    /// Supplied package files do not form a valid Complete Package Tree.
1481    #[error("external package fulfillment for {spec} is malformed at `{path}`: {message}")]
1482    MalformedExternalPackageTree {
1483        spec: PackageSpec,
1484        path: String,
1485        message: String,
1486    },
1487    /// No fulfillment was supplied for declared external Font Containers.
1488    #[error("external font fulfillment is unavailable for {containers:?}")]
1489    MissingExternalFontFulfillment {
1490        containers: Vec<FontContainerIdentity>,
1491    },
1492    /// Supplied bytes do not match the exact required Font Container.
1493    #[error("external font fulfillment for {expected:?} supplied {actual:?}")]
1494    MismatchedExternalFontContainer {
1495        expected: FontContainerIdentity,
1496        actual: FontContainerIdentity,
1497        expected_length: u64,
1498        actual_length: u64,
1499    },
1500    /// Verified container bytes do not contain one declared face.
1501    #[error("external font container {container:?} has no valid face at index {index}")]
1502    MalformedExternalFontContainer {
1503        container: FontContainerIdentity,
1504        index: u32,
1505    },
1506}
1507
1508#[derive(Debug, thiserror::Error)]
1509pub(crate) enum PackCompilationPreparationError {
1510    #[error("{0}")]
1511    RequestRejected(CompilationRequestRejection),
1512    #[error("{outcome}")]
1513    Operation {
1514        outcome: CompilationOperationOutcome,
1515        request_inventory: Box<CompilationRequestInventory>,
1516        compilation_identity: CompilationIdentity,
1517        fulfillments: Box<CompilationFulfillmentReport>,
1518    },
1519}
1520
1521/// Compiles one private adapter world through the embedded Typst implementation.
1522#[cfg(test)]
1523pub(crate) fn compile_world(
1524    world: &dyn World,
1525    output: &CompilationOutputSpecification,
1526) -> Result<CompilationOutput, CompileError> {
1527    compile_with_default_pdf_timestamp(world, output, || world.today(None).map(Timestamp::new_utc))
1528}
1529
1530/// Compiles a validated Pack and retains operational fulfillment evidence.
1531#[allow(clippy::result_large_err)]
1532pub fn compile(
1533    request: PackCompilationRequest,
1534) -> Result<CompilationReport, CompilationRequestRejection> {
1535    let (world, kernel) = match prepare_pack_compilation(request) {
1536        Ok(prepared) => prepared,
1537        Err(PackCompilationPreparationError::Operation {
1538            outcome,
1539            request_inventory,
1540            compilation_identity,
1541            fulfillments,
1542        }) => {
1543            return Ok(CompilationReport {
1544                outcome: CompilationReportOutcome::Operation {
1545                    outcome,
1546                    request_inventory,
1547                    compilation_identity,
1548                },
1549                fulfillments: *fulfillments,
1550            });
1551        }
1552        Err(PackCompilationPreparationError::RequestRejected(rejection)) => {
1553            return Err(rejection);
1554        }
1555    };
1556    let execution = compile_pack_kernel(&world, kernel);
1557    Ok(CompilationReport {
1558        outcome: CompilationReportOutcome::Result(Box::new(execution.result)),
1559        fulfillments: execution.fulfillments,
1560    })
1561}
1562
1563pub(crate) struct PreparedPackCompilationKernel {
1564    request_inventory: CompilationRequestInventory,
1565    compilation_identity: CompilationIdentity,
1566    engine_identity: EngineIdentity,
1567    exporter_identity: ExporterIdentity,
1568    page_selection_implies_untagged_pdf: bool,
1569    fulfillments: CompilationFulfillmentReport,
1570}
1571
1572pub(crate) struct PackCompilationExecution {
1573    pub(crate) result: CompilationResult,
1574    #[cfg(feature = "diagnostics")]
1575    pub(crate) presentation: PackCompilationPresentation,
1576    pub(crate) fulfillments: CompilationFulfillmentReport,
1577}
1578
1579#[cfg(feature = "diagnostics")]
1580pub(crate) enum PackCompilationPresentation {
1581    Succeeded {
1582        warnings: EcoVec<SourceDiagnostic>,
1583        pack_warnings: EcoVec<SourceDiagnostic>,
1584    },
1585    Diagnostics {
1586        errors: EcoVec<SourceDiagnostic>,
1587        warnings: EcoVec<SourceDiagnostic>,
1588        pack_warnings: EcoVec<SourceDiagnostic>,
1589    },
1590    PngExport {
1591        error: String,
1592        warnings: EcoVec<SourceDiagnostic>,
1593        pack_warnings: EcoVec<SourceDiagnostic>,
1594    },
1595}
1596
1597#[allow(clippy::result_large_err)]
1598pub(crate) fn prepare_pack_compilation(
1599    request: PackCompilationRequest,
1600) -> Result<(PackWorld, PreparedPackCompilationKernel), PackCompilationPreparationError> {
1601    let PackCompilationRequest {
1602        pack,
1603        output_specification: mut output,
1604        inputs,
1605        overrides,
1606        features,
1607        document_time,
1608        package_fulfillments,
1609        font_fulfillments,
1610    } = request;
1611    let mut request_issues = vec![];
1612    if overrides.value.pack_identity != pack.identity() {
1613        request_issues.push(CompilationRequestIssue::OverrideSetPackMismatch);
1614    }
1615    if features
1616        .iter()
1617        .any(|feature| feature.value == Feature::Bundle)
1618    {
1619        request_issues.push(CompilationRequestIssue::UnsupportedBundleFeature);
1620    }
1621    if let DocumentTime::UnixTimestamp(timestamp) = document_time.value
1622        && typst_kit::datetime::Time::fixed_timestamp(timestamp).is_err()
1623    {
1624        request_issues.push(CompilationRequestIssue::InvalidDocumentTimestamp);
1625    }
1626    let page_selection_implies_untagged_pdf;
1627    let output_origins = match &mut output.value {
1628        CompilationOutputSpecification::Png(specification) => {
1629            let mut pixels_per_inch = output.origin;
1630            if specification
1631                .pixels_per_inch
1632                .is_some_and(|ppi| !ppi.is_finite() || ppi <= 0.0)
1633            {
1634                request_issues.push(CompilationRequestIssue::InvalidPpi);
1635            }
1636            if specification.pixels_per_inch.is_none() {
1637                specification.pixels_per_inch = Some(default_png_ppi());
1638                pixels_per_inch = RequestValueOrigin::CoreDefaulted;
1639            }
1640            page_selection_implies_untagged_pdf = false;
1641            CompilationOutputOrigins::Png { pixels_per_inch }
1642        }
1643        CompilationOutputSpecification::Pdf(specification) => {
1644            let mut tags = output.origin;
1645            let mut creation_time = output.origin;
1646            if let Err(error) = validate_pdf_standards(&specification.standards) {
1647                request_issues.push(CompilationRequestIssue::InvalidPdfStandards(error));
1648            }
1649            page_selection_implies_untagged_pdf = !specification.page_selection.ranges().is_empty()
1650                && specification.tags.is_auto()
1651                && PdfOptions::default().tagged;
1652            if specification.tags.is_auto() {
1653                specification.tags = Smart::Custom(
1654                    PdfOptions::default().tagged
1655                        && specification.page_selection.ranges().is_empty(),
1656                );
1657                tags = if specification.page_selection.ranges().is_empty() {
1658                    RequestValueOrigin::CoreDefaulted
1659                } else {
1660                    RequestValueOrigin::CoreDerived
1661                };
1662            }
1663            if matches!(
1664                specification.creation_timestamp,
1665                CreationTimestamp::Automatic
1666            ) {
1667                specification.creation_timestamp = CreationTimestamp::Omit;
1668                creation_time = RequestValueOrigin::CoreDefaulted;
1669            }
1670            CompilationOutputOrigins::Pdf {
1671                tags,
1672                creation_time,
1673            }
1674        }
1675        CompilationOutputSpecification::Svg(_) => {
1676            page_selection_implies_untagged_pdf = false;
1677            CompilationOutputOrigins::Svg
1678        }
1679        CompilationOutputSpecification::Html(_) => {
1680            page_selection_implies_untagged_pdf = false;
1681            CompilationOutputOrigins::Html
1682        }
1683    };
1684    let selected_features = [Feature::Html, Feature::Bundle, Feature::A11yExtras]
1685        .into_iter()
1686        .filter_map(|value| {
1687            features
1688                .iter()
1689                .find(|feature| feature.value == value)
1690                .copied()
1691        })
1692        .collect::<Vec<_>>();
1693    let mut effective_features = selected_features.clone();
1694    if matches!(&output.value, CompilationOutputSpecification::Html(_)) {
1695        effective_features.retain(|feature| feature.value != Feature::Html);
1696        effective_features.push(EffectiveEngineFeature {
1697            value: Feature::Html,
1698            origin: RequestValueOrigin::CoreDerived,
1699        });
1700    }
1701    let engine_identity = EmbeddedTypst::engine_identity();
1702    let exporter_identity = EmbeddedTypst::exporter_identity(output.value.format());
1703    let raw_inputs = inputs.value;
1704    let total_key_bytes = raw_inputs.iter().map(|(key, _)| key.len()).sum();
1705    let total_value_repr_bytes = raw_inputs.iter().map(|(_, value)| value.repr().len()).sum();
1706    let inputs_commitment = typst::utils::hash128(&(
1707        "typst-pack-inputs-v1",
1708        total_key_bytes,
1709        total_value_repr_bytes,
1710        &raw_inputs,
1711    ));
1712    let override_inventory = PackOverridesInventory(
1713        overrides
1714            .value
1715            .replacements
1716            .iter()
1717            .map(|(path, data)| PackOverrideInventoryEntry {
1718                path: path.clone(),
1719                byte_len: data.len(),
1720                commitment: typst::utils::hash128(&(
1721                    "typst-pack-override-v1+typst-0.15",
1722                    "project-file",
1723                    overrides.value.pack_identity,
1724                    path,
1725                    data.len(),
1726                    data,
1727                )),
1728            })
1729            .collect(),
1730    );
1731    let request_inventory = CompilationRequestInventory {
1732        output_specification: output,
1733        output_origins,
1734        inputs: EffectiveRequestValue::new(
1735            TypstInputsInventory {
1736                commitment: inputs_commitment,
1737                entry_count: raw_inputs.len(),
1738                total_key_bytes,
1739                total_value_repr_bytes,
1740            },
1741            inputs.origin,
1742        ),
1743        overrides: EffectiveRequestValue::new(override_inventory, overrides.origin),
1744        selected_features,
1745        features: effective_features,
1746        document_time,
1747    };
1748    if !request_issues.is_empty() {
1749        return Err(PackCompilationPreparationError::RequestRejected(
1750            CompilationRequestRejection::new(request_issues, request_inventory),
1751        ));
1752    }
1753    let compilation_identity = compilation_identity(
1754        &pack,
1755        &request_inventory,
1756        engine_identity,
1757        exporter_identity,
1758    );
1759    let fulfillments = CompilationFulfillmentReport {
1760        packages: pack
1761            .package_requirements()
1762            .iter()
1763            .map(|requirement| {
1764                let supplied = package_fulfillments.get(&requirement.spec().to_string());
1765                PackageFulfillmentReport {
1766                    spec: requirement.spec().clone(),
1767                    tree_identity: requirement.tree_identity(),
1768                    embedded: requirement.is_embedded(),
1769                    provenance: supplied.and_then(|value| value.provenance.clone()),
1770                    cache_hit: supplied.is_some_and(|value| value.cache_hit),
1771                }
1772            })
1773            .collect(),
1774        fonts: pack
1775            .font_requirements()
1776            .iter()
1777            .map(|requirement| {
1778                let supplied = font_fulfillments.get(&requirement.container_identity());
1779                FontFulfillmentReport {
1780                    container_identity: requirement.container_identity(),
1781                    embedded: requirement.is_embedded(),
1782                    provenance: supplied.and_then(|value| value.provenance.clone()),
1783                    licensing: supplied.and_then(|value| value.licensing.clone()),
1784                }
1785            })
1786            .collect(),
1787    };
1788    let package_files = package_fulfillments
1789        .into_iter()
1790        .map(|(spec, fulfillment)| (spec, fulfillment.files))
1791        .collect();
1792    let fulfillment_bytes = font_fulfillments
1793        .into_iter()
1794        .map(|(identity, fulfillment)| (identity, fulfillment.data))
1795        .collect();
1796    let dependencies = pack
1797        .materialize_compilation_dependency_snapshot(package_files, &fulfillment_bytes)
1798        .map_err(|error| {
1799            let outcome = match error {
1800                CompilationDependencySnapshotError::Package(error) => package_tree_outcome(*error),
1801                CompilationDependencySnapshotError::Font(error) => font_catalog_outcome(error),
1802            };
1803            PackCompilationPreparationError::Operation {
1804                outcome,
1805                request_inventory: Box::new(request_inventory.clone()),
1806                compilation_identity,
1807                fulfillments: Box::new(fulfillments.clone()),
1808            }
1809        })?;
1810
1811    let world = PackWorld::new(
1812        pack,
1813        dependencies,
1814        overrides.value.replacements,
1815        raw_inputs,
1816        request_inventory
1817            .features
1818            .iter()
1819            .map(|feature| feature.value)
1820            .collect(),
1821        request_inventory.document_time.value,
1822    )
1823    .expect("preflighted Pack World inputs must remain valid");
1824
1825    Ok((
1826        world,
1827        PreparedPackCompilationKernel {
1828            request_inventory,
1829            compilation_identity,
1830            engine_identity,
1831            exporter_identity,
1832            page_selection_implies_untagged_pdf,
1833            fulfillments,
1834        },
1835    ))
1836}
1837
1838pub(crate) fn compile_pack_kernel(
1839    world: &PackWorld,
1840    kernel: PreparedPackCompilationKernel,
1841) -> PackCompilationExecution {
1842    let traced = WorldTrace::new(world);
1843    let compiled = compile_with_default_pdf_timestamp(
1844        &traced,
1845        kernel.request_inventory.output_specification.value(),
1846        || None,
1847    );
1848    let access_trace = traced.snapshot();
1849    match compiled {
1850        Ok(output) => {
1851            #[cfg(feature = "diagnostics")]
1852            let warnings = output.warnings.clone();
1853            #[cfg(feature = "diagnostics")]
1854            let mut presentation_pack_warnings = output.pack_warnings.clone();
1855            #[cfg(feature = "diagnostics")]
1856            if kernel.page_selection_implies_untagged_pdf {
1857                presentation_pack_warnings.push(page_selection_pdf_tags_warning());
1858            }
1859            let diagnostics = project_diagnostics(
1860                &traced,
1861                output.warnings,
1862                DiagnosticPhase::Compilation,
1863                DiagnosticProducer::Engine(kernel.engine_identity),
1864            );
1865            let pack_warnings = project_pack_warnings(
1866                output.pack_warnings,
1867                kernel.page_selection_implies_untagged_pdf,
1868            );
1869            PackCompilationExecution {
1870                result: assemble_compilation_result(
1871                    &kernel,
1872                    CompilationStatus::Succeeded,
1873                    output.artifacts,
1874                    diagnostics,
1875                    pack_warnings,
1876                    output.source_page_count,
1877                    access_trace,
1878                ),
1879                #[cfg(feature = "diagnostics")]
1880                presentation: PackCompilationPresentation::Succeeded {
1881                    warnings,
1882                    pack_warnings: presentation_pack_warnings,
1883                },
1884                fulfillments: kernel.fulfillments,
1885            }
1886        }
1887        Err(CompileError::Diagnostics {
1888            errors,
1889            warnings,
1890            pack_warnings,
1891            phase,
1892            source_page_count,
1893        }) => {
1894            #[cfg(feature = "diagnostics")]
1895            let mut presentation_pack_warnings = pack_warnings.clone();
1896            #[cfg(feature = "diagnostics")]
1897            if kernel.page_selection_implies_untagged_pdf {
1898                presentation_pack_warnings.push(page_selection_pdf_tags_warning());
1899            }
1900            #[cfg(feature = "diagnostics")]
1901            let presentation = PackCompilationPresentation::Diagnostics {
1902                errors: errors.clone(),
1903                warnings: warnings.clone(),
1904                pack_warnings: presentation_pack_warnings,
1905            };
1906            let mut diagnostics = project_diagnostics(
1907                &traced,
1908                warnings,
1909                DiagnosticPhase::Compilation,
1910                DiagnosticProducer::Engine(kernel.engine_identity),
1911            );
1912            let producer = match phase {
1913                DiagnosticPhase::Compilation => DiagnosticProducer::Engine(kernel.engine_identity),
1914                DiagnosticPhase::Export => DiagnosticProducer::Exporter(kernel.exporter_identity),
1915            };
1916            diagnostics.extend(project_diagnostics(&traced, errors, phase, producer));
1917            PackCompilationExecution {
1918                result: assemble_compilation_result(
1919                    &kernel,
1920                    CompilationStatus::Rejected,
1921                    vec![],
1922                    diagnostics,
1923                    project_pack_warnings(
1924                        pack_warnings,
1925                        kernel.page_selection_implies_untagged_pdf,
1926                    ),
1927                    source_page_count,
1928                    access_trace,
1929                ),
1930                #[cfg(feature = "diagnostics")]
1931                presentation,
1932                fulfillments: kernel.fulfillments,
1933            }
1934        }
1935        Err(CompileError::PngExport {
1936            message,
1937            warnings,
1938            pack_warnings,
1939            source_page_count,
1940            source_page_number,
1941        }) => {
1942            #[cfg(feature = "diagnostics")]
1943            let mut presentation_pack_warnings = pack_warnings.clone();
1944            #[cfg(feature = "diagnostics")]
1945            if kernel.page_selection_implies_untagged_pdf {
1946                presentation_pack_warnings.push(page_selection_pdf_tags_warning());
1947            }
1948            #[cfg(feature = "diagnostics")]
1949            let presentation = PackCompilationPresentation::PngExport {
1950                error: format!("PNG export failed for source page {source_page_number}: {message}"),
1951                warnings: warnings.clone(),
1952                pack_warnings: presentation_pack_warnings,
1953            };
1954            let mut diagnostics = project_diagnostics(
1955                &traced,
1956                warnings,
1957                DiagnosticPhase::Compilation,
1958                DiagnosticProducer::Engine(kernel.engine_identity),
1959            );
1960            diagnostics.push(CompilationDiagnostic {
1961                severity: DiagnosticSeverity::Error,
1962                message,
1963                span: LogicalSpan {
1964                    logical_path: None,
1965                    byte_range: None,
1966                },
1967                hints: vec![],
1968                trace: vec![],
1969                phase: DiagnosticPhase::Export,
1970                producer: DiagnosticProducer::Exporter(kernel.exporter_identity),
1971                source_page_number: Some(source_page_number),
1972            });
1973            PackCompilationExecution {
1974                result: assemble_compilation_result(
1975                    &kernel,
1976                    CompilationStatus::Rejected,
1977                    vec![],
1978                    diagnostics,
1979                    project_pack_warnings(
1980                        pack_warnings,
1981                        kernel.page_selection_implies_untagged_pdf,
1982                    ),
1983                    Some(source_page_count),
1984                    access_trace,
1985                ),
1986                #[cfg(feature = "diagnostics")]
1987                presentation,
1988                fulfillments: kernel.fulfillments,
1989            }
1990        }
1991        Err(CompileError::InvalidPdfStandards(error)) => {
1992            unreachable!("PDF standards are validated during request preparation: {error}");
1993        }
1994    }
1995}
1996
1997fn assemble_compilation_result(
1998    kernel: &PreparedPackCompilationKernel,
1999    status: CompilationStatus,
2000    artifacts: Vec<CompilationArtifact>,
2001    diagnostics: Vec<CompilationDiagnostic>,
2002    pack_warnings: Vec<PackCompilationWarning>,
2003    source_page_count: Option<usize>,
2004    access_trace: CompilationAccessTrace,
2005) -> CompilationResult {
2006    finalize_result(CompilationResult {
2007        status,
2008        artifacts,
2009        diagnostics,
2010        pack_warnings,
2011        document: document_summary(
2012            kernel.request_inventory.output_specification.value(),
2013            source_page_count,
2014        ),
2015        access_trace,
2016        result_identity: CompilationResultIdentity(0),
2017        request_inventory: kernel.request_inventory.clone(),
2018        compilation_identity: kernel.compilation_identity,
2019        engine_identity: kernel.engine_identity,
2020        exporter_identity: kernel.exporter_identity,
2021    })
2022}
2023
2024fn document_summary(
2025    output: &CompilationOutputSpecification,
2026    source_page_count: Option<usize>,
2027) -> CompilationDocumentSummary {
2028    CompilationDocumentSummary {
2029        target: output.target(),
2030        source_page_count,
2031    }
2032}
2033
2034fn finalize_result(mut result: CompilationResult) -> CompilationResult {
2035    let artifacts = result
2036        .artifacts
2037        .iter()
2038        .map(|artifact| {
2039            (
2040                artifact.format,
2041                artifact.source_page_number,
2042                artifact.bytes.len(),
2043                typst::utils::hash128(&artifact.bytes),
2044            )
2045        })
2046        .collect::<Vec<_>>();
2047    result.result_identity = CompilationResultIdentity(typst::utils::hash128(&(
2048        "typst-pack-compilation-result-v1",
2049        result.compilation_identity,
2050        result.status,
2051        result.document,
2052        &result.diagnostics,
2053        &result.pack_warnings,
2054        &result.access_trace,
2055        artifacts,
2056    )));
2057    result
2058}
2059
2060pub(crate) fn package_tree_outcome(error: PackageTreeError) -> CompilationOperationOutcome {
2061    match error {
2062        PackageTreeError::Missing { packages } => {
2063            CompilationOperationOutcome::MissingExternalPackageFulfillment { packages }
2064        }
2065        PackageTreeError::Mismatched {
2066            spec,
2067            expected,
2068            actual,
2069            expected_file_count,
2070            actual_file_count,
2071            expected_byte_length,
2072            actual_byte_length,
2073        } => CompilationOperationOutcome::MismatchedExternalPackageTree {
2074            spec,
2075            expected,
2076            actual,
2077            expected_file_count,
2078            actual_file_count,
2079            expected_byte_length,
2080            actual_byte_length,
2081        },
2082        PackageTreeError::Malformed {
2083            spec,
2084            path,
2085            message,
2086        } => CompilationOperationOutcome::MalformedExternalPackageTree {
2087            spec,
2088            path,
2089            message,
2090        },
2091    }
2092}
2093
2094fn font_catalog_outcome(error: FontCatalogError) -> CompilationOperationOutcome {
2095    match error {
2096        FontCatalogError::Missing { containers } => {
2097            CompilationOperationOutcome::MissingExternalFontFulfillment { containers }
2098        }
2099        FontCatalogError::Mismatched {
2100            expected,
2101            actual,
2102            expected_length,
2103            actual_length,
2104        } => CompilationOperationOutcome::MismatchedExternalFontContainer {
2105            expected,
2106            actual,
2107            expected_length,
2108            actual_length,
2109        },
2110        FontCatalogError::Malformed { container, index } => {
2111            CompilationOperationOutcome::MalformedExternalFontContainer { container, index }
2112        }
2113    }
2114}
2115
2116fn compilation_identity(
2117    pack: &Pack,
2118    inventory: &CompilationRequestInventory,
2119    engine_identity: EngineIdentity,
2120    exporter_identity: ExporterIdentity,
2121) -> CompilationIdentity {
2122    let output_digest = match inventory.output_specification.value() {
2123        CompilationOutputSpecification::Pdf(specification) => {
2124            let page_selection = canonical_page_selection(&specification.page_selection);
2125            let mut standards = specification
2126                .standards
2127                .iter()
2128                .map(pdf_standard_identity)
2129                .collect::<Vec<_>>();
2130            standards.sort_unstable();
2131            typst::utils::hash128(&(
2132                "pdf",
2133                &page_selection,
2134                &specification.identifier,
2135                &specification.creator,
2136                specification.tags,
2137                specification.creation_timestamp,
2138                standards,
2139                specification.pretty,
2140            ))
2141        }
2142        CompilationOutputSpecification::Png(specification) => {
2143            let page_selection = canonical_page_selection(&specification.page_selection);
2144            typst::utils::hash128(&(
2145                "png",
2146                &page_selection,
2147                specification.pixels_per_inch.map(f64::to_bits),
2148                specification.render_bleed,
2149            ))
2150        }
2151        CompilationOutputSpecification::Svg(specification) => {
2152            let page_selection = canonical_page_selection(&specification.page_selection);
2153            typst::utils::hash128(&(
2154                "svg",
2155                &page_selection,
2156                specification.render_bleed,
2157                specification.pretty,
2158            ))
2159        }
2160        CompilationOutputSpecification::Html(specification) => {
2161            typst::utils::hash128(&("html", specification.pretty))
2162        }
2163    };
2164    let feature_values = inventory
2165        .features
2166        .iter()
2167        .map(|feature| feature.value)
2168        .collect::<Vec<_>>();
2169    let (document_time, document_timestamp) = inventory.document_time.value.identity_projection();
2170    CompilationIdentity(typst::utils::hash128(&(
2171        "typst-pack-compilation-v1",
2172        pack.identity(),
2173        inventory.output_specification.value().format(),
2174        output_digest,
2175        inventory.inputs.value.commitment,
2176        inventory
2177            .overrides
2178            .value
2179            .iter()
2180            .map(|entry| (&entry.path, entry.byte_len, entry.commitment))
2181            .collect::<Vec<_>>(),
2182        feature_values,
2183        document_time,
2184        document_timestamp,
2185        engine_identity,
2186        exporter_identity,
2187    )))
2188}
2189
2190fn canonical_page_selection(selection: &PageSelection) -> (bool, Vec<(usize, usize)>) {
2191    let selects_all = selection.ranges.is_empty();
2192    let mut ranges = selection
2193        .ranges
2194        .iter()
2195        .filter_map(|range| {
2196            let start = range.start().map_or(1, NonZeroUsize::get);
2197            let end = range.end().map_or(usize::MAX, NonZeroUsize::get);
2198            (start <= end).then_some((start, end))
2199        })
2200        .collect::<Vec<_>>();
2201    ranges.sort_unstable();
2202    let mut canonical: Vec<(usize, usize)> = vec![];
2203    for (start, end) in ranges {
2204        if let Some(last) = canonical.last_mut()
2205            && start <= last.1.saturating_add(1)
2206        {
2207            last.1 = last.1.max(end);
2208        } else {
2209            canonical.push((start, end));
2210        }
2211    }
2212    (selects_all, canonical)
2213}
2214
2215fn pdf_standard_identity(standard: &PdfStandard) -> &'static str {
2216    match standard {
2217        PdfStandard::V_1_4 => "1.4",
2218        PdfStandard::V_1_5 => "1.5",
2219        PdfStandard::V_1_6 => "1.6",
2220        PdfStandard::V_1_7 => "1.7",
2221        PdfStandard::V_2_0 => "2.0",
2222        PdfStandard::A_1b => "a-1b",
2223        PdfStandard::A_1a => "a-1a",
2224        PdfStandard::A_2b => "a-2b",
2225        PdfStandard::A_2u => "a-2u",
2226        PdfStandard::A_2a => "a-2a",
2227        PdfStandard::A_3b => "a-3b",
2228        PdfStandard::A_3u => "a-3u",
2229        PdfStandard::A_3a => "a-3a",
2230        PdfStandard::A_4 => "a-4",
2231        PdfStandard::A_4f => "a-4f",
2232        PdfStandard::A_4e => "a-4e",
2233        PdfStandard::Ua_1 => "ua-1",
2234        _ => unreachable!("all standards in pinned typst-pdf are represented"),
2235    }
2236}
2237
2238pub(crate) fn compile_with_default_pdf_timestamp(
2239    world: &dyn World,
2240    specification: &CompilationOutputSpecification,
2241    default_pdf_timestamp: impl FnOnce() -> Option<Timestamp>,
2242) -> Result<CompilationOutput, CompileError> {
2243    let _compilation_timing = typst_timing::TimingScope::new("typst-pack compilation");
2244    if let CompilationOutputSpecification::Html(specification) = specification {
2245        let pack_warnings = EcoVec::new();
2246        let Warned { output, warnings } = EmbeddedTypst::compile_html(world);
2247        let document = output.map_err(|errors| CompileError::Diagnostics {
2248            errors,
2249            warnings: warnings.clone(),
2250            pack_warnings: pack_warnings.clone(),
2251            phase: DiagnosticPhase::Compilation,
2252            source_page_count: None,
2253        })?;
2254        let _export_timing = typst_timing::TimingScope::new("export");
2255        let bytes = EmbeddedTypst::export_html(
2256            &document,
2257            &typst_html::HtmlOptions {
2258                pretty: specification.pretty,
2259            },
2260        )
2261        .map_err(|errors| CompileError::Diagnostics {
2262            errors,
2263            warnings: warnings.clone(),
2264            pack_warnings: pack_warnings.clone(),
2265            phase: DiagnosticPhase::Export,
2266            source_page_count: None,
2267        })?;
2268        return Ok(CompilationOutput {
2269            artifacts: vec![CompilationArtifact {
2270                format: OutputFormat::Html,
2271                bytes,
2272                source_page_number: None,
2273            }],
2274            warnings,
2275            pack_warnings,
2276            source_page_count: None,
2277        });
2278    }
2279
2280    let Warned {
2281        output,
2282        warnings: compile_warnings,
2283    } = EmbeddedTypst::compile_paged(world);
2284    let warnings = compile_warnings;
2285    let mut pack_warnings = EcoVec::new();
2286    if let CompilationOutputSpecification::Pdf(specification) = specification
2287        && !specification.page_selection.ranges().is_empty()
2288        && specification.tags.is_auto()
2289        && PdfOptions::default().tagged
2290    {
2291        pack_warnings.push(page_selection_pdf_tags_warning());
2292    }
2293    let document = output.map_err(|errors| CompileError::Diagnostics {
2294        errors,
2295        warnings: warnings.clone(),
2296        pack_warnings: pack_warnings.clone(),
2297        phase: DiagnosticPhase::Compilation,
2298        source_page_count: None,
2299    })?;
2300    let source_page_count = document.pages().len();
2301    let artifacts = {
2302        let _export_timing = typst_timing::TimingScope::new("export");
2303        match specification {
2304            CompilationOutputSpecification::Pdf(specification) => {
2305                let standards = validate_pdf_standards(&specification.standards)
2306                    .map_err(CompileError::InvalidPdfStandards)?;
2307                let timestamp = match specification.creation_timestamp {
2308                    CreationTimestamp::Automatic => default_pdf_timestamp(),
2309                    CreationTimestamp::Explicit(timestamp) => Some(timestamp),
2310                    CreationTimestamp::Omit => None,
2311                };
2312                let pdf_options = PdfOptions {
2313                    ident: specification.identifier.clone(),
2314                    creator: specification.creator.clone(),
2315                    timestamp,
2316                    page_ranges: specification.page_selection.typst_page_ranges(),
2317                    standards,
2318                    tagged: match specification.tags {
2319                        Smart::Auto => {
2320                            PdfOptions::default().tagged
2321                                && specification.page_selection.ranges().is_empty()
2322                        }
2323                        Smart::Custom(tagged) => tagged,
2324                    },
2325                    pretty: specification.pretty,
2326                };
2327                let pdf = EmbeddedTypst::export_pdf(&document, &pdf_options).map_err(|errors| {
2328                    CompileError::Diagnostics {
2329                        errors,
2330                        warnings: warnings.clone(),
2331                        pack_warnings: pack_warnings.clone(),
2332                        phase: DiagnosticPhase::Export,
2333                        source_page_count: Some(source_page_count),
2334                    }
2335                })?;
2336                vec![CompilationArtifact {
2337                    format: OutputFormat::Pdf,
2338                    bytes: pdf,
2339                    source_page_number: None,
2340                }]
2341            }
2342            CompilationOutputSpecification::Png(specification) => {
2343                let pixels_per_inch = specification
2344                    .pixels_per_inch
2345                    .unwrap_or_else(default_png_ppi);
2346                let render_options = typst_render::RenderOptions {
2347                    pixel_per_pt: (pixels_per_inch / 72.0).into(),
2348                    render_bleed: specification.render_bleed,
2349                };
2350                let pages =
2351                    selected_pages(&document, &specification.page_selection).collect::<Vec<_>>();
2352                let export = |(source_page_number, page)| {
2353                    let bytes =
2354                        EmbeddedTypst::export_png(page, &render_options).map_err(|message| {
2355                            CompileError::PngExport {
2356                                message,
2357                                warnings: warnings.clone(),
2358                                pack_warnings: pack_warnings.clone(),
2359                                source_page_count,
2360                                source_page_number,
2361                            }
2362                        })?;
2363                    Ok::<_, CompileError>(CompilationArtifact {
2364                        format: OutputFormat::Png,
2365                        bytes,
2366                        source_page_number: Some(source_page_number),
2367                    })
2368                };
2369                #[cfg(feature = "parallel")]
2370                let artifacts = pages
2371                    .into_par_iter()
2372                    .map(export)
2373                    .collect::<Result<Vec<_>, _>>()?;
2374                #[cfg(not(feature = "parallel"))]
2375                let artifacts = pages
2376                    .into_iter()
2377                    .map(export)
2378                    .collect::<Result<Vec<_>, _>>()?;
2379                artifacts
2380            }
2381            CompilationOutputSpecification::Svg(specification) => {
2382                let svg_options = typst_svg::SvgOptions {
2383                    render_bleed: specification.render_bleed,
2384                    pretty: specification.pretty,
2385                };
2386                let pages =
2387                    selected_pages(&document, &specification.page_selection).collect::<Vec<_>>();
2388                let export = |(source_page_number, page)| CompilationArtifact {
2389                    format: OutputFormat::Svg,
2390                    bytes: EmbeddedTypst::export_svg(page, &svg_options),
2391                    source_page_number: Some(source_page_number),
2392                };
2393                #[cfg(feature = "parallel")]
2394                let artifacts = pages.into_par_iter().map(export).collect();
2395                #[cfg(not(feature = "parallel"))]
2396                let artifacts = pages.into_iter().map(export).collect();
2397                artifacts
2398            }
2399            CompilationOutputSpecification::Html(_) => unreachable!("handled above"),
2400        }
2401    };
2402    Ok(CompilationOutput {
2403        artifacts,
2404        warnings,
2405        pack_warnings,
2406        source_page_count: Some(source_page_count),
2407    })
2408}
2409
2410pub(crate) fn validate_pdf_standards(
2411    standards: &[PdfStandard],
2412) -> Result<PdfStandards, PdfStandardsValidationError> {
2413    PdfStandards::new(standards).map_err(|error| PdfStandardsValidationError {
2414        message: error.message().to_string(),
2415        hints: error.hints().iter().map(ToString::to_string).collect(),
2416    })
2417}
2418
2419#[cfg(feature = "diagnostics")]
2420pub(crate) fn pdf_standard_requiring_tags(standards: &[PdfStandard]) -> Option<&'static str> {
2421    standards.iter().find_map(|standard| match standard {
2422        PdfStandard::A_1a => Some("PDF/A-1a"),
2423        PdfStandard::A_2a => Some("PDF/A-2a"),
2424        PdfStandard::A_3a => Some("PDF/A-3a"),
2425        PdfStandard::Ua_1 => Some("PDF/UA-1"),
2426        _ => None,
2427    })
2428}
2429
2430fn selected_pages<'a>(
2431    document: &'a PagedDocument,
2432    page_selection: &'a PageSelection,
2433) -> impl Iterator<Item = (NonZeroUsize, &'a typst_layout::Page)> {
2434    let ranges = page_selection.typst_page_ranges();
2435    document
2436        .pages()
2437        .iter()
2438        .enumerate()
2439        .filter(move |(index, _)| {
2440            ranges.as_ref().is_none_or(|ranges| {
2441                NonZeroUsize::new(index + 1).is_some_and(|number| ranges.includes_page(number))
2442            })
2443        })
2444        .map(|(index, page)| (NonZeroUsize::new(index + 1).unwrap(), page))
2445}
2446
2447fn default_png_ppi() -> f64 {
2448    typst_render::RenderOptions::default().pixel_per_pt.get() * 72.0
2449}
2450
2451fn project_diagnostics(
2452    world: &dyn World,
2453    diagnostics: impl IntoIterator<Item = SourceDiagnostic>,
2454    phase: DiagnosticPhase,
2455    producer: DiagnosticProducer,
2456) -> Vec<CompilationDiagnostic> {
2457    diagnostics
2458        .into_iter()
2459        .map(|diagnostic| CompilationDiagnostic {
2460            severity: match diagnostic.severity {
2461                Severity::Error => DiagnosticSeverity::Error,
2462                Severity::Warning => DiagnosticSeverity::Warning,
2463            },
2464            message: diagnostic.message.into(),
2465            span: logical_span(world, diagnostic.span),
2466            hints: diagnostic
2467                .hints
2468                .into_iter()
2469                .map(|hint| DiagnosticHint {
2470                    message: hint.v.into(),
2471                    span: logical_span(world, hint.span),
2472                })
2473                .collect(),
2474            trace: diagnostic
2475                .trace
2476                .into_iter()
2477                .map(|trace| {
2478                    let (kind, value) = match trace.v {
2479                        Tracepoint::Call(value) => (TracepointKind::Call, value.map(String::from)),
2480                        Tracepoint::Show(value) => (TracepointKind::Show, Some(value.into())),
2481                        Tracepoint::Import(value) => (TracepointKind::Import, Some(value.into())),
2482                        Tracepoint::Include(value) => (TracepointKind::Include, Some(value.into())),
2483                    };
2484                    DiagnosticTracepoint {
2485                        kind,
2486                        value,
2487                        span: logical_span(world, trace.span.into()),
2488                    }
2489                })
2490                .collect(),
2491            phase,
2492            producer,
2493            source_page_number: None,
2494        })
2495        .collect()
2496}
2497
2498fn project_pack_warnings(
2499    warnings: impl IntoIterator<Item = SourceDiagnostic>,
2500    page_selection_implies_untagged_pdf: bool,
2501) -> Vec<PackCompilationWarning> {
2502    warnings
2503        .into_iter()
2504        .chain(page_selection_implies_untagged_pdf.then(page_selection_pdf_tags_warning))
2505        .map(|warning| PackCompilationWarning {
2506            message: warning.message.into(),
2507            hints: warning
2508                .hints
2509                .into_iter()
2510                .map(|hint| hint.v.into())
2511                .collect(),
2512        })
2513        .collect()
2514}
2515
2516fn page_selection_pdf_tags_warning() -> SourceDiagnostic {
2517    SourceDiagnostic::warning(Span::detached(), "using --pages implies --no-pdf-tags").with_hints([
2518        "the resulting PDF will be inaccessible".into(),
2519        "add --no-pdf-tags to silence this warning".into(),
2520    ])
2521}
2522
2523fn logical_span(world: &dyn World, span: DiagSpan) -> LogicalSpan {
2524    LogicalSpan {
2525        logical_path: span.id().map(logical_path),
2526        byte_range: world.range(span),
2527    }
2528}
2529
2530#[cfg(test)]
2531mod result_identity_tests {
2532    use super::*;
2533
2534    #[test]
2535    fn compilation_trace_retains_missing_font_requests() {
2536        let trace = CompilationAccessTrace::from_observations(BTreeSet::from([
2537            CompilationAccessObservation::new(
2538                CompilationAccessKind::Font,
2539                "font-index:7".to_owned(),
2540                Some(7),
2541                CompilationAccessOutcome::Missing,
2542            ),
2543        ]));
2544
2545        let observation = trace.observations().next().unwrap();
2546        assert_eq!(observation.kind(), CompilationAccessKind::Font);
2547        assert_eq!(observation.logical_path(), "font-index:7");
2548        assert_eq!(observation.font_index(), Some(7));
2549        assert_eq!(observation.outcome(), &CompilationAccessOutcome::Missing);
2550    }
2551
2552    #[test]
2553    fn result_identity_binds_each_post_execution_projection() {
2554        let pack = Pack::builder("main.typ")
2555            .file(
2556                "main.typ",
2557                b"#set page(width: 20pt, height: 10pt, margin: 0pt)\n#rect(width: 1pt, height: 1pt)".to_vec(),
2558            )
2559            .unwrap()
2560            .build()
2561            .unwrap();
2562        let report = compile(PackCompilationRequest::new(
2563            pack,
2564            CompilationOutputSpecification::Svg(SvgOutputSpecification::default()),
2565        ))
2566        .unwrap();
2567        let base = report.result().unwrap().clone();
2568        let identity = base.result_identity;
2569
2570        let mut status = base.clone();
2571        status.status = CompilationStatus::Rejected;
2572        assert_ne!(finalize_result(status).result_identity, identity);
2573
2574        let mut document = base.clone();
2575        document.document.source_page_count = Some(2);
2576        assert_ne!(finalize_result(document).result_identity, identity);
2577
2578        let mut diagnostics = base.clone();
2579        diagnostics.diagnostics.push(CompilationDiagnostic {
2580            severity: DiagnosticSeverity::Warning,
2581            message: "identity warning".to_owned(),
2582            span: LogicalSpan {
2583                logical_path: None,
2584                byte_range: None,
2585            },
2586            hints: vec![],
2587            trace: vec![],
2588            phase: DiagnosticPhase::Compilation,
2589            producer: DiagnosticProducer::Engine(base.engine_identity),
2590            source_page_number: None,
2591        });
2592        assert_ne!(finalize_result(diagnostics).result_identity, identity);
2593
2594        let mut access = base.clone();
2595        access
2596            .access_trace
2597            .observations
2598            .insert(CompilationAccessObservation {
2599                kind: CompilationAccessKind::File,
2600                logical_path: "project:missing.txt".to_owned(),
2601                font_index: None,
2602                outcome: CompilationAccessOutcome::Missing,
2603            });
2604        assert_ne!(finalize_result(access).result_identity, identity);
2605
2606        let mut artifact = base;
2607        artifact.artifacts[0].bytes.push(0);
2608        assert_ne!(finalize_result(artifact).result_identity, identity);
2609    }
2610}