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