Skip to main content

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