use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroUsize;
use std::ops::Range;
use ecow::EcoVec;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use typst::diag::{Severity, SourceDiagnostic, Tracepoint, Warned};
use typst::foundations::{Bytes, Datetime, Dict, Repr, Smart};
use typst::syntax::package::PackageSpec;
use typst::syntax::{DiagSpan, Span};
use typst::{Feature, World, WorldExt};
use typst_layout::PagedDocument;
use typst_pdf::{PdfOptions, PdfStandard, PdfStandards, Timestamp};
use crate::embedded::EmbeddedTypst;
use crate::pack::{CompilationDependencySnapshotError, FontCatalogError, PackageTreeError};
use crate::world::PackWorld;
use crate::world_trace::{WorldTrace, logical_path};
use crate::{FontContainerIdentity, Pack, PackageTreeIdentity};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ImplementationIdentity {
implementation: &'static str,
version: &'static str,
source_checksum: &'static str,
target: &'static str,
target_features: &'static str,
feature_set: &'static str,
debug_assertions: bool,
}
impl ImplementationIdentity {
const fn new(
implementation: &'static str,
version: &'static str,
source_checksum: &'static str,
) -> Self {
Self {
implementation,
version,
source_checksum,
target: env!("TYPST_PACK_TARGET"),
target_features: env!("TYPST_PACK_CARGO_CFG_TARGET_FEATURE"),
feature_set: env!("TYPST_PACK_FEATURE_SET"),
debug_assertions: cfg!(debug_assertions),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EngineIdentity(ImplementationIdentity);
impl EngineIdentity {
pub(crate) const fn new(
implementation: &'static str,
version: &'static str,
source_checksum: &'static str,
) -> Self {
Self(ImplementationIdentity::new(
implementation,
version,
source_checksum,
))
}
pub fn implementation(self) -> &'static str {
self.0.implementation
}
pub fn version(self) -> &'static str {
self.0.version
}
pub fn source_checksum(self) -> &'static str {
self.0.source_checksum
}
pub fn target(self) -> &'static str {
self.0.target
}
pub fn target_features(self) -> &'static str {
self.0.target_features
}
pub fn feature_set(self) -> &'static str {
self.0.feature_set
}
pub fn debug_assertions(self) -> bool {
self.0.debug_assertions
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExporterIdentity(ImplementationIdentity);
impl ExporterIdentity {
pub(crate) const fn new(
implementation: &'static str,
version: &'static str,
source_checksum: &'static str,
) -> Self {
Self(ImplementationIdentity::new(
implementation,
version,
source_checksum,
))
}
pub fn implementation(self) -> &'static str {
self.0.implementation
}
pub fn version(self) -> &'static str {
self.0.version
}
pub fn source_checksum(self) -> &'static str {
self.0.source_checksum
}
pub fn target(self) -> &'static str {
self.0.target
}
pub fn target_features(self) -> &'static str {
self.0.target_features
}
pub fn feature_set(self) -> &'static str {
self.0.feature_set
}
pub fn debug_assertions(self) -> bool {
self.0.debug_assertions
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OutputFormat {
Pdf,
Png,
Svg,
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TypstTarget {
Paged,
Html,
}
impl OutputFormat {
pub fn extension(self) -> &'static str {
match self {
Self::Pdf => "pdf",
Self::Png => "png",
Self::Svg => "svg",
Self::Html => "html",
}
}
}
#[derive(Debug, Clone)]
pub struct PdfOutputSpecification {
pub page_selection: PageSelection,
pub standards: Vec<PdfStandard>,
pub identifier: Smart<String>,
pub creator: Smart<Option<String>>,
pub tags: Smart<bool>,
pub creation_timestamp: CreationTimestamp,
pub pretty: bool,
}
impl Default for PdfOutputSpecification {
fn default() -> Self {
let pdf = PdfOptions::default();
Self {
page_selection: PageSelection::default(),
standards: vec![],
identifier: pdf.ident,
creator: pdf.creator,
tags: Smart::Auto,
creation_timestamp: CreationTimestamp::Automatic,
pretty: pdf.pretty,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PngOutputSpecification {
pub page_selection: PageSelection,
pub pixels_per_inch: Option<f64>,
pub render_bleed: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SvgOutputSpecification {
pub page_selection: PageSelection,
pub render_bleed: bool,
pub pretty: bool,
}
#[derive(Debug, Clone, Default)]
pub struct HtmlOutputSpecification {
pub pretty: bool,
}
#[derive(Debug, Clone)]
pub enum CompilationOutputSpecification {
Pdf(PdfOutputSpecification),
Png(PngOutputSpecification),
Svg(SvgOutputSpecification),
Html(HtmlOutputSpecification),
}
impl CompilationOutputSpecification {
pub fn format(&self) -> OutputFormat {
match self {
Self::Pdf(_) => OutputFormat::Pdf,
Self::Png(_) => OutputFormat::Png,
Self::Svg(_) => OutputFormat::Svg,
Self::Html(_) => OutputFormat::Html,
}
}
fn target(&self) -> TypstTarget {
match self {
Self::Html(_) => TypstTarget::Html,
Self::Pdf(_) | Self::Png(_) | Self::Svg(_) => TypstTarget::Paged,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RequestValueOrigin {
CallerSupplied,
CoreDefaulted,
CoreDerived,
AdapterResolved,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
pub enum DocumentTime {
Absent,
Fixed(Datetime),
UnixTimestamp(i64),
}
impl DocumentTime {
fn identity_projection(self) -> (Option<Datetime>, Option<i64>) {
match self {
Self::Absent => (None, None),
Self::Fixed(datetime) => (Some(datetime), None),
Self::UnixTimestamp(timestamp) => (None, Some(timestamp)),
}
}
}
#[derive(Debug, Clone)]
pub struct EffectiveRequestValue<T> {
value: T,
origin: RequestValueOrigin,
}
impl<T> EffectiveRequestValue<T> {
fn new(value: T, origin: RequestValueOrigin) -> Self {
Self { value, origin }
}
pub fn value(&self) -> &T {
&self.value
}
pub fn origin(&self) -> RequestValueOrigin {
self.origin
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EffectiveEngineFeature {
value: Feature,
origin: RequestValueOrigin,
}
impl EffectiveEngineFeature {
pub fn value(self) -> Feature {
self.value
}
pub fn origin(self) -> RequestValueOrigin {
self.origin
}
}
#[derive(Debug, Clone)]
pub struct CompilationRequestInventory {
output_specification: EffectiveRequestValue<CompilationOutputSpecification>,
output_origins: CompilationOutputOrigins,
inputs: EffectiveRequestValue<TypstInputsInventory>,
overrides: EffectiveRequestValue<PackOverridesInventory>,
selected_features: Vec<EffectiveEngineFeature>,
features: Vec<EffectiveEngineFeature>,
document_time: EffectiveRequestValue<DocumentTime>,
}
impl CompilationRequestInventory {
pub fn output_specification(&self) -> &EffectiveRequestValue<CompilationOutputSpecification> {
&self.output_specification
}
pub fn inputs(&self) -> &EffectiveRequestValue<TypstInputsInventory> {
&self.inputs
}
pub fn overrides(&self) -> &EffectiveRequestValue<PackOverridesInventory> {
&self.overrides
}
pub fn output_origins(&self) -> CompilationOutputOrigins {
self.output_origins
}
pub fn features(&self) -> &[EffectiveEngineFeature] {
&self.features
}
pub fn selected_features(&self) -> &[EffectiveEngineFeature] {
&self.selected_features
}
pub fn document_time(&self) -> &EffectiveRequestValue<DocumentTime> {
&self.document_time
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompilationOutputOrigins {
Pdf {
tags: RequestValueOrigin,
creation_time: RequestValueOrigin,
},
Png {
pixels_per_inch: RequestValueOrigin,
},
Svg,
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TypstInputsInventory {
commitment: u128,
entry_count: usize,
total_key_bytes: usize,
total_value_repr_bytes: usize,
}
impl TypstInputsInventory {
pub fn schema(self) -> &'static str {
"typst-pack-inputs-v1+typst-0.15"
}
pub fn commitment(self) -> [u8; 16] {
self.commitment.to_be_bytes()
}
pub fn entry_count(self) -> usize {
self.entry_count
}
pub fn total_key_bytes(self) -> usize {
self.total_key_bytes
}
pub fn total_value_repr_bytes(self) -> usize {
self.total_value_repr_bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackOverrideInventoryEntry {
path: String,
byte_len: usize,
commitment: u128,
}
impl PackOverrideInventoryEntry {
pub fn schema(&self) -> &'static str {
"typst-pack-override-v1+typst-0.15"
}
pub fn path(&self) -> &str {
&self.path
}
pub fn byte_len(&self) -> usize {
self.byte_len
}
pub fn commitment(&self) -> [u8; 16] {
self.commitment.to_be_bytes()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PackOverridesInventory(Vec<PackOverrideInventoryEntry>);
impl PackOverridesInventory {
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &PackOverrideInventoryEntry> {
self.0.iter()
}
}
#[derive(Debug, Clone)]
pub struct PackOverrideSet {
pack_identity: crate::PackIdentity,
project_paths: BTreeSet<String>,
replacements: BTreeMap<String, Bytes>,
}
impl PackOverrideSet {
pub fn new(pack: &Pack) -> Self {
Self {
pack_identity: pack.identity(),
project_paths: pack.files().map(|(path, _)| path.to_owned()).collect(),
replacements: BTreeMap::new(),
}
}
pub fn replace(
mut self,
path: impl AsRef<str>,
data: impl Into<Vec<u8>>,
) -> Result<Self, PackOverrideSetError> {
let supplied = path.as_ref();
let path = Pack::canonical_project_path(supplied).map_err(|message| {
PackOverrideSetError::InvalidProjectPath {
path: supplied.to_owned(),
message,
}
})?;
if self.replacements.contains_key(&path) {
return Err(PackOverrideSetError::DuplicateProjectPath { path });
}
if !self.project_paths.contains(&path) {
return Err(PackOverrideSetError::MissingProjectPath { path });
}
self.replacements.insert(path, Bytes::new(data.into()));
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PackOverrideSetError {
#[error("invalid Pack Override project path `{path}`: {message}")]
InvalidProjectPath { path: String, message: String },
#[error("Pack Override path `{path}` is declared more than once")]
DuplicateProjectPath { path: String },
#[error("Pack Override path `{path}` is not a contained project file")]
MissingProjectPath { path: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompilationIdentity(u128);
impl CompilationIdentity {
pub fn kind(self) -> &'static str {
"compilation"
}
pub fn schema(self) -> &'static str {
"typst-pack-compilation-v1"
}
pub fn algorithm(self) -> &'static str {
"typst-hash128-0.15"
}
pub fn digest(self) -> [u8; 16] {
self.0.to_be_bytes()
}
}
pub struct PackCompilationRequest {
pack: Pack,
output_specification: EffectiveRequestValue<CompilationOutputSpecification>,
inputs: EffectiveRequestValue<Dict>,
overrides: EffectiveRequestValue<PackOverrideSet>,
features: Vec<EffectiveEngineFeature>,
document_time: EffectiveRequestValue<DocumentTime>,
package_fulfillments: BTreeMap<String, PackageTreeFulfillment>,
font_fulfillments: BTreeMap<FontContainerIdentity, FontContainerFulfillment>,
}
#[derive(Debug, Clone)]
pub struct PackageTreeFulfillment {
files: Vec<(String, Bytes)>,
provenance: Option<String>,
cache_hit: bool,
}
impl PackageTreeFulfillment {
pub fn new<I, P, D>(files: I) -> Self
where
I: IntoIterator<Item = (P, D)>,
P: Into<String>,
D: Into<Vec<u8>>,
{
Self {
files: files
.into_iter()
.map(|(path, data)| (path.into(), Bytes::new(data.into())))
.collect(),
provenance: None,
cache_hit: false,
}
}
pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
self.provenance = Some(provenance.into());
self
}
pub fn cache_hit(mut self, cache_hit: bool) -> Self {
self.cache_hit = cache_hit;
self
}
}
#[derive(Debug, Clone)]
pub struct FontContainerFulfillment {
data: Bytes,
provenance: Option<String>,
licensing: Option<String>,
}
impl FontContainerFulfillment {
pub fn new(data: impl Into<Vec<u8>>) -> Self {
Self {
data: Bytes::new(data.into()),
provenance: None,
licensing: None,
}
}
pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
self.provenance = Some(provenance.into());
self
}
pub fn licensing(mut self, licensing: impl Into<String>) -> Self {
self.licensing = Some(licensing.into());
self
}
}
impl PackCompilationRequest {
pub fn new(pack: Pack, output_specification: CompilationOutputSpecification) -> Self {
let overrides = PackOverrideSet::new(&pack);
Self {
pack,
output_specification: EffectiveRequestValue::new(
output_specification,
RequestValueOrigin::CallerSupplied,
),
inputs: EffectiveRequestValue::new(Dict::new(), RequestValueOrigin::CoreDefaulted),
overrides: EffectiveRequestValue::new(overrides, RequestValueOrigin::CoreDefaulted),
features: Vec::new(),
document_time: EffectiveRequestValue::new(
DocumentTime::Absent,
RequestValueOrigin::CoreDefaulted,
),
package_fulfillments: BTreeMap::new(),
font_fulfillments: BTreeMap::new(),
}
}
#[cfg(feature = "diagnostics")]
#[doc(hidden)]
pub fn adapter_resolved_output(mut self) -> Self {
self.output_specification.origin = RequestValueOrigin::AdapterResolved;
self
}
pub fn inputs(mut self, inputs: Dict) -> Self {
self.inputs = EffectiveRequestValue::new(inputs, RequestValueOrigin::CallerSupplied);
self
}
pub fn adapter_resolved_inputs(mut self, inputs: Dict) -> Self {
self.inputs = EffectiveRequestValue::new(inputs, RequestValueOrigin::AdapterResolved);
self
}
pub fn overrides(mut self, overrides: PackOverrideSet) -> Self {
self.overrides = EffectiveRequestValue::new(overrides, RequestValueOrigin::CallerSupplied);
self
}
pub fn feature(mut self, feature: Feature) -> Self {
self.features.push(EffectiveEngineFeature {
value: feature,
origin: RequestValueOrigin::CallerSupplied,
});
self
}
pub fn adapter_resolved_feature(mut self, feature: Feature) -> Self {
self.features.push(EffectiveEngineFeature {
value: feature,
origin: RequestValueOrigin::AdapterResolved,
});
self
}
pub fn document_time(mut self, document_time: DocumentTime) -> Self {
self.document_time =
EffectiveRequestValue::new(document_time, RequestValueOrigin::CallerSupplied);
self
}
pub fn adapter_resolved_document_time(mut self, document_time: DocumentTime) -> Self {
self.document_time =
EffectiveRequestValue::new(document_time, RequestValueOrigin::AdapterResolved);
self
}
pub fn font_fulfillment(
mut self,
expected: FontContainerIdentity,
fulfillment: FontContainerFulfillment,
) -> Self {
self.font_fulfillments.insert(expected, fulfillment);
self
}
pub fn package_fulfillment(
mut self,
spec: PackageSpec,
fulfillment: PackageTreeFulfillment,
) -> Self {
self.package_fulfillments
.insert(spec.to_string(), fulfillment);
self
}
}
#[derive(Debug, Clone, Copy, Default, Hash)]
pub enum CreationTimestamp {
#[default]
Automatic,
Explicit(Timestamp),
Omit,
}
pub type PageRange = std::ops::RangeInclusive<Option<NonZeroUsize>>;
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct PageSelection {
ranges: Vec<PageRange>,
}
impl PageSelection {
pub fn all() -> Self {
Self::default()
}
pub fn new(ranges: Vec<PageRange>) -> Self {
Self { ranges }
}
pub fn ranges(&self) -> &[PageRange] {
&self.ranges
}
fn typst_page_ranges(&self) -> Option<typst::layout::PageRanges> {
(!self.ranges.is_empty()).then(|| typst::layout::PageRanges::new(self.ranges.clone()))
}
}
pub fn parse_page_selection(text: &str) -> Result<PageSelection, String> {
text.split(',')
.map(|part| {
let part = part.trim();
let parse = |value: &str| -> Result<NonZeroUsize, String> {
if value == "0" {
Err("page numbers start at one".to_owned())
} else {
value
.parse::<NonZeroUsize>()
.map_err(|_| format!("`{value}` is not a valid page number"))
}
};
match part
.split('-')
.map(str::trim)
.collect::<Vec<_>>()
.as_slice()
{
[] | [""] => Err("page export range must not be empty".to_owned()),
[single] => {
let page = parse(single)?;
Ok(Some(page)..=Some(page))
}
["", ""] => Err("page export range must have start or end".to_owned()),
[start, ""] => Ok(Some(parse(start)?)..=None),
["", end] => Ok(None..=Some(parse(end)?)),
[start, end] => {
let start = parse(start)?;
let end = parse(end)?;
if start > end {
Err("page export range must end at a page after the start".to_owned())
} else {
Ok(Some(start)..=Some(end))
}
}
_ => Err("page export range must have a single hyphen".to_owned()),
}
})
.collect::<Result<Vec<_>, _>>()
.map(PageSelection::new)
}
#[derive(Debug, Clone)]
pub struct CompilationArtifact {
format: OutputFormat,
bytes: Vec<u8>,
source_page_number: Option<NonZeroUsize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CompilationStatus {
Succeeded,
Rejected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticPhase {
Compilation,
Export,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticProducer {
Engine(EngineIdentity),
Exporter(ExporterIdentity),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LogicalSpan {
logical_path: Option<String>,
byte_range: Option<Range<usize>>,
}
impl LogicalSpan {
pub fn logical_path(&self) -> Option<&str> {
self.logical_path.as_deref()
}
pub fn byte_range(&self) -> Option<&Range<usize>> {
self.byte_range.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DiagnosticHint {
message: String,
span: LogicalSpan,
}
impl DiagnosticHint {
pub fn message(&self) -> &str {
&self.message
}
pub fn span(&self) -> &LogicalSpan {
&self.span
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TracepointKind {
Call,
Show,
Import,
Include,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DiagnosticTracepoint {
kind: TracepointKind,
value: Option<String>,
span: LogicalSpan,
}
impl DiagnosticTracepoint {
pub fn kind(&self) -> TracepointKind {
self.kind
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn span(&self) -> &LogicalSpan {
&self.span
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CompilationDiagnostic {
severity: DiagnosticSeverity,
message: String,
span: LogicalSpan,
hints: Vec<DiagnosticHint>,
trace: Vec<DiagnosticTracepoint>,
phase: DiagnosticPhase,
producer: DiagnosticProducer,
source_page_number: Option<NonZeroUsize>,
}
impl CompilationDiagnostic {
pub fn severity(&self) -> DiagnosticSeverity {
self.severity
}
pub fn message(&self) -> &str {
&self.message
}
pub fn span(&self) -> &LogicalSpan {
&self.span
}
pub fn hints(&self) -> &[DiagnosticHint] {
&self.hints
}
pub fn trace(&self) -> &[DiagnosticTracepoint] {
&self.trace
}
pub fn phase(&self) -> DiagnosticPhase {
self.phase
}
pub fn producer(&self) -> DiagnosticProducer {
self.producer
}
pub fn source_page_number(&self) -> Option<NonZeroUsize> {
self.source_page_number
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompilationDocumentSummary {
target: TypstTarget,
source_page_count: Option<usize>,
}
impl CompilationDocumentSummary {
pub fn target(self) -> TypstTarget {
self.target
}
pub fn source_page_count(self) -> Option<usize> {
self.source_page_count
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CompilationAccessKind {
Source,
File,
Font,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CompilationAccessOutcome {
Read {
byte_length: usize,
digest: [u8; 16],
},
Missing,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CompilationAccessObservation {
kind: CompilationAccessKind,
logical_path: String,
font_index: Option<usize>,
outcome: CompilationAccessOutcome,
}
impl CompilationAccessObservation {
pub(crate) fn new(
kind: CompilationAccessKind,
logical_path: String,
font_index: Option<usize>,
outcome: CompilationAccessOutcome,
) -> Self {
Self {
kind,
logical_path,
font_index,
outcome,
}
}
pub fn kind(&self) -> CompilationAccessKind {
self.kind
}
pub fn logical_path(&self) -> &str {
&self.logical_path
}
pub fn font_index(&self) -> Option<usize> {
self.font_index
}
pub fn outcome(&self) -> &CompilationAccessOutcome {
&self.outcome
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct CompilationAccessTrace {
observations: BTreeSet<CompilationAccessObservation>,
}
impl CompilationAccessTrace {
pub fn observations(&self) -> impl Iterator<Item = &CompilationAccessObservation> {
self.observations.iter()
}
pub(crate) fn from_observations(observations: BTreeSet<CompilationAccessObservation>) -> Self {
Self { observations }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompilationResultIdentity(u128);
impl CompilationResultIdentity {
pub fn kind(self) -> &'static str {
"compilation-result"
}
pub fn schema(self) -> &'static str {
"typst-pack-compilation-result-v1"
}
pub fn algorithm(self) -> &'static str {
"typst-hash128-0.15"
}
pub fn digest(self) -> [u8; 16] {
self.0.to_be_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageFulfillmentReport {
spec: PackageSpec,
tree_identity: PackageTreeIdentity,
embedded: bool,
provenance: Option<String>,
cache_hit: bool,
}
impl PackageFulfillmentReport {
pub fn spec(&self) -> &PackageSpec {
&self.spec
}
pub fn tree_identity(&self) -> PackageTreeIdentity {
self.tree_identity
}
pub fn embedded(&self) -> bool {
self.embedded
}
pub fn provenance(&self) -> Option<&str> {
self.provenance.as_deref()
}
pub fn cache_hit(&self) -> bool {
self.cache_hit
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FontFulfillmentReport {
container_identity: FontContainerIdentity,
embedded: bool,
provenance: Option<String>,
licensing: Option<String>,
}
impl FontFulfillmentReport {
pub fn container_identity(&self) -> FontContainerIdentity {
self.container_identity
}
pub fn embedded(&self) -> bool {
self.embedded
}
pub fn provenance(&self) -> Option<&str> {
self.provenance.as_deref()
}
pub fn licensing(&self) -> Option<&str> {
self.licensing.as_deref()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CompilationFulfillmentReport {
packages: Vec<PackageFulfillmentReport>,
fonts: Vec<FontFulfillmentReport>,
}
impl CompilationFulfillmentReport {
pub fn packages(&self) -> &[PackageFulfillmentReport] {
&self.packages
}
pub fn fonts(&self) -> &[FontFulfillmentReport] {
&self.fonts
}
}
#[derive(Debug, Clone)]
pub struct CompilationReport {
outcome: CompilationReportOutcome,
fulfillments: CompilationFulfillmentReport,
}
#[derive(Debug, Clone)]
pub enum CompilationReportOutcome {
Result(Box<CompilationResult>),
Operation {
outcome: CompilationOperationOutcome,
request_inventory: Box<CompilationRequestInventory>,
compilation_identity: CompilationIdentity,
},
}
impl CompilationReport {
pub fn outcome(&self) -> &CompilationReportOutcome {
&self.outcome
}
pub fn result(&self) -> Option<&CompilationResult> {
match &self.outcome {
CompilationReportOutcome::Result(result) => Some(result.as_ref()),
CompilationReportOutcome::Operation { .. } => None,
}
}
pub fn fulfillments(&self) -> &CompilationFulfillmentReport {
&self.fulfillments
}
}
#[derive(Debug, Clone)]
pub struct CompilationResult {
status: CompilationStatus,
artifacts: Vec<CompilationArtifact>,
diagnostics: Vec<CompilationDiagnostic>,
pack_warnings: Vec<PackCompilationWarning>,
document: CompilationDocumentSummary,
access_trace: CompilationAccessTrace,
result_identity: CompilationResultIdentity,
request_inventory: CompilationRequestInventory,
compilation_identity: CompilationIdentity,
engine_identity: EngineIdentity,
exporter_identity: ExporterIdentity,
}
impl CompilationResult {
pub fn status(&self) -> CompilationStatus {
self.status
}
pub fn artifacts(&self) -> &[CompilationArtifact] {
&self.artifacts
}
pub fn diagnostics(&self) -> &[CompilationDiagnostic] {
&self.diagnostics
}
pub fn pack_warnings(&self) -> &[PackCompilationWarning] {
&self.pack_warnings
}
pub fn source_page_count(&self) -> Option<usize> {
self.document.source_page_count
}
pub fn document(&self) -> CompilationDocumentSummary {
self.document
}
pub fn access_trace(&self) -> &CompilationAccessTrace {
&self.access_trace
}
pub fn result_identity(&self) -> CompilationResultIdentity {
self.result_identity
}
pub fn request_inventory(&self) -> &CompilationRequestInventory {
&self.request_inventory
}
pub fn compilation_identity(&self) -> CompilationIdentity {
self.compilation_identity
}
pub fn engine_identity(&self) -> EngineIdentity {
self.engine_identity
}
pub fn exporter_identity(&self) -> ExporterIdentity {
self.exporter_identity
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PackCompilationWarning {
message: String,
hints: Vec<String>,
}
impl PackCompilationWarning {
pub fn message(&self) -> &str {
&self.message
}
pub fn hints(&self) -> &[String] {
&self.hints
}
}
impl CompilationArtifact {
pub fn format(&self) -> OutputFormat {
self.format
}
pub fn source_page_number(&self) -> Option<NonZeroUsize> {
self.source_page_number
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
#[derive(Debug, Clone)]
pub(crate) struct CompilationOutput {
pub artifacts: Vec<CompilationArtifact>,
pub warnings: EcoVec<SourceDiagnostic>,
pack_warnings: EcoVec<SourceDiagnostic>,
source_page_count: Option<usize>,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum CompileError {
#[error(transparent)]
InvalidPdfStandards(#[from] PdfStandardsValidationError),
#[error("compilation failed with {} error(s)", errors.len())]
Diagnostics {
errors: EcoVec<SourceDiagnostic>,
warnings: EcoVec<SourceDiagnostic>,
pack_warnings: EcoVec<SourceDiagnostic>,
phase: DiagnosticPhase,
source_page_count: Option<usize>,
},
#[error("PNG export failed for source page {source_page_number}: {message}")]
PngExport {
message: String,
warnings: EcoVec<SourceDiagnostic>,
pack_warnings: EcoVec<SourceDiagnostic>,
source_page_count: usize,
source_page_number: NonZeroUsize,
},
}
#[derive(Debug, thiserror::Error)]
#[error("invalid PDF standards: {message}")]
pub struct PdfStandardsValidationError {
message: String,
hints: Vec<String>,
}
impl PdfStandardsValidationError {
pub fn message(&self) -> &str {
&self.message
}
pub fn hints(&self) -> &[String] {
&self.hints
}
pub fn into_parts(self) -> (String, Vec<String>) {
(self.message, self.hints)
}
}
#[derive(Debug, thiserror::Error)]
pub enum CompilationRequestIssue {
#[error("the Typst Bundle feature is not supported for Pack compilation")]
UnsupportedBundleFeature,
#[error("PNG pixels per inch must be finite and greater than zero")]
InvalidPpi,
#[error(transparent)]
InvalidPdfStandards(PdfStandardsValidationError),
#[error("the Pack Override Set is bound to a different Pack")]
OverrideSetPackMismatch,
#[error("the document-time UNIX timestamp is out of range")]
InvalidDocumentTimestamp,
}
#[derive(Debug)]
pub struct CompilationRequestRejection {
issues: Vec<CompilationRequestIssue>,
request_inventory: Box<CompilationRequestInventory>,
}
impl CompilationRequestRejection {
pub fn issues(&self) -> &[CompilationRequestIssue] {
&self.issues
}
pub fn request_inventory(&self) -> &CompilationRequestInventory {
&self.request_inventory
}
fn new(
issues: Vec<CompilationRequestIssue>,
request_inventory: CompilationRequestInventory,
) -> Self {
debug_assert!(!issues.is_empty());
Self {
issues,
request_inventory: Box::new(request_inventory),
}
}
}
impl std::fmt::Display for CompilationRequestRejection {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let [issue] = self.issues.as_slice() {
issue.fmt(formatter)
} else {
formatter.write_str("the compilation request contains multiple invalid values")
}
}
}
impl std::error::Error for CompilationRequestRejection {}
#[derive(Debug, Clone, thiserror::Error)]
pub enum CompilationOperationOutcome {
#[error("external package fulfillment is unavailable for {packages:?}")]
MissingExternalPackageFulfillment { packages: Vec<PackageSpec> },
#[error("external package fulfillment for {spec} supplied {actual:?}, expected {expected:?}")]
MismatchedExternalPackageTree {
spec: PackageSpec,
expected: PackageTreeIdentity,
actual: PackageTreeIdentity,
expected_file_count: u64,
actual_file_count: u64,
expected_byte_length: u64,
actual_byte_length: u64,
},
#[error("external package fulfillment for {spec} is malformed at `{path}`: {message}")]
MalformedExternalPackageTree {
spec: PackageSpec,
path: String,
message: String,
},
#[error("external font fulfillment is unavailable for {containers:?}")]
MissingExternalFontFulfillment {
containers: Vec<FontContainerIdentity>,
},
#[error("external font fulfillment for {expected:?} supplied {actual:?}")]
MismatchedExternalFontContainer {
expected: FontContainerIdentity,
actual: FontContainerIdentity,
expected_length: u64,
actual_length: u64,
},
#[error("external font container {container:?} has no valid face at index {index}")]
MalformedExternalFontContainer {
container: FontContainerIdentity,
index: u32,
},
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum PackCompilationPreparationError {
#[error("{0}")]
RequestRejected(CompilationRequestRejection),
#[error("{outcome}")]
Operation {
outcome: CompilationOperationOutcome,
request_inventory: Box<CompilationRequestInventory>,
compilation_identity: CompilationIdentity,
fulfillments: Box<CompilationFulfillmentReport>,
},
}
#[cfg(test)]
pub(crate) fn compile_world(
world: &dyn World,
output: &CompilationOutputSpecification,
) -> Result<CompilationOutput, CompileError> {
compile_with_default_pdf_timestamp(world, output, || world.today(None).map(Timestamp::new_utc))
}
#[allow(clippy::result_large_err)]
pub fn compile(
request: PackCompilationRequest,
) -> Result<CompilationReport, CompilationRequestRejection> {
let (world, kernel) = match prepare_pack_compilation(request) {
Ok(prepared) => prepared,
Err(PackCompilationPreparationError::Operation {
outcome,
request_inventory,
compilation_identity,
fulfillments,
}) => {
return Ok(CompilationReport {
outcome: CompilationReportOutcome::Operation {
outcome,
request_inventory,
compilation_identity,
},
fulfillments: *fulfillments,
});
}
Err(PackCompilationPreparationError::RequestRejected(rejection)) => {
return Err(rejection);
}
};
let execution = compile_pack_kernel(&world, kernel);
Ok(CompilationReport {
outcome: CompilationReportOutcome::Result(Box::new(execution.result)),
fulfillments: execution.fulfillments,
})
}
pub(crate) struct PreparedPackCompilationKernel {
request_inventory: CompilationRequestInventory,
compilation_identity: CompilationIdentity,
engine_identity: EngineIdentity,
exporter_identity: ExporterIdentity,
page_selection_implies_untagged_pdf: bool,
fulfillments: CompilationFulfillmentReport,
}
pub(crate) struct PackCompilationExecution {
pub(crate) result: CompilationResult,
#[cfg(feature = "diagnostics")]
pub(crate) presentation: PackCompilationPresentation,
pub(crate) fulfillments: CompilationFulfillmentReport,
}
#[cfg(feature = "diagnostics")]
pub(crate) enum PackCompilationPresentation {
Succeeded {
warnings: EcoVec<SourceDiagnostic>,
pack_warnings: EcoVec<SourceDiagnostic>,
},
Diagnostics {
errors: EcoVec<SourceDiagnostic>,
warnings: EcoVec<SourceDiagnostic>,
pack_warnings: EcoVec<SourceDiagnostic>,
},
PngExport {
error: String,
warnings: EcoVec<SourceDiagnostic>,
pack_warnings: EcoVec<SourceDiagnostic>,
},
}
#[allow(clippy::result_large_err)]
pub(crate) fn prepare_pack_compilation(
request: PackCompilationRequest,
) -> Result<(PackWorld, PreparedPackCompilationKernel), PackCompilationPreparationError> {
let PackCompilationRequest {
pack,
output_specification: mut output,
inputs,
overrides,
features,
document_time,
package_fulfillments,
font_fulfillments,
} = request;
let mut request_issues = vec![];
if overrides.value.pack_identity != pack.identity() {
request_issues.push(CompilationRequestIssue::OverrideSetPackMismatch);
}
if features
.iter()
.any(|feature| feature.value == Feature::Bundle)
{
request_issues.push(CompilationRequestIssue::UnsupportedBundleFeature);
}
if let DocumentTime::UnixTimestamp(timestamp) = document_time.value
&& typst_kit::datetime::Time::fixed_timestamp(timestamp).is_err()
{
request_issues.push(CompilationRequestIssue::InvalidDocumentTimestamp);
}
let page_selection_implies_untagged_pdf;
let output_origins = match &mut output.value {
CompilationOutputSpecification::Png(specification) => {
let mut pixels_per_inch = output.origin;
if specification
.pixels_per_inch
.is_some_and(|ppi| !ppi.is_finite() || ppi <= 0.0)
{
request_issues.push(CompilationRequestIssue::InvalidPpi);
}
if specification.pixels_per_inch.is_none() {
specification.pixels_per_inch = Some(default_png_ppi());
pixels_per_inch = RequestValueOrigin::CoreDefaulted;
}
page_selection_implies_untagged_pdf = false;
CompilationOutputOrigins::Png { pixels_per_inch }
}
CompilationOutputSpecification::Pdf(specification) => {
let mut tags = output.origin;
let mut creation_time = output.origin;
if let Err(error) = validate_pdf_standards(&specification.standards) {
request_issues.push(CompilationRequestIssue::InvalidPdfStandards(error));
}
page_selection_implies_untagged_pdf = !specification.page_selection.ranges().is_empty()
&& specification.tags.is_auto()
&& PdfOptions::default().tagged;
if specification.tags.is_auto() {
specification.tags = Smart::Custom(
PdfOptions::default().tagged
&& specification.page_selection.ranges().is_empty(),
);
tags = if specification.page_selection.ranges().is_empty() {
RequestValueOrigin::CoreDefaulted
} else {
RequestValueOrigin::CoreDerived
};
}
if matches!(
specification.creation_timestamp,
CreationTimestamp::Automatic
) {
specification.creation_timestamp = CreationTimestamp::Omit;
creation_time = RequestValueOrigin::CoreDefaulted;
}
CompilationOutputOrigins::Pdf {
tags,
creation_time,
}
}
CompilationOutputSpecification::Svg(_) => {
page_selection_implies_untagged_pdf = false;
CompilationOutputOrigins::Svg
}
CompilationOutputSpecification::Html(_) => {
page_selection_implies_untagged_pdf = false;
CompilationOutputOrigins::Html
}
};
let selected_features = [Feature::Html, Feature::Bundle, Feature::A11yExtras]
.into_iter()
.filter_map(|value| {
features
.iter()
.find(|feature| feature.value == value)
.copied()
})
.collect::<Vec<_>>();
let mut effective_features = selected_features.clone();
if matches!(&output.value, CompilationOutputSpecification::Html(_)) {
effective_features.retain(|feature| feature.value != Feature::Html);
effective_features.push(EffectiveEngineFeature {
value: Feature::Html,
origin: RequestValueOrigin::CoreDerived,
});
}
let engine_identity = EmbeddedTypst::engine_identity();
let exporter_identity = EmbeddedTypst::exporter_identity(output.value.format());
let raw_inputs = inputs.value;
let total_key_bytes = raw_inputs.iter().map(|(key, _)| key.len()).sum();
let total_value_repr_bytes = raw_inputs.iter().map(|(_, value)| value.repr().len()).sum();
let inputs_commitment = typst::utils::hash128(&(
"typst-pack-inputs-v1",
total_key_bytes,
total_value_repr_bytes,
&raw_inputs,
));
let override_inventory = PackOverridesInventory(
overrides
.value
.replacements
.iter()
.map(|(path, data)| PackOverrideInventoryEntry {
path: path.clone(),
byte_len: data.len(),
commitment: typst::utils::hash128(&(
"typst-pack-override-v1+typst-0.15",
"project-file",
overrides.value.pack_identity,
path,
data.len(),
data,
)),
})
.collect(),
);
let request_inventory = CompilationRequestInventory {
output_specification: output,
output_origins,
inputs: EffectiveRequestValue::new(
TypstInputsInventory {
commitment: inputs_commitment,
entry_count: raw_inputs.len(),
total_key_bytes,
total_value_repr_bytes,
},
inputs.origin,
),
overrides: EffectiveRequestValue::new(override_inventory, overrides.origin),
selected_features,
features: effective_features,
document_time,
};
if !request_issues.is_empty() {
return Err(PackCompilationPreparationError::RequestRejected(
CompilationRequestRejection::new(request_issues, request_inventory),
));
}
let compilation_identity = compilation_identity(
&pack,
&request_inventory,
engine_identity,
exporter_identity,
);
let fulfillments = CompilationFulfillmentReport {
packages: pack
.package_requirements()
.iter()
.map(|requirement| {
let supplied = package_fulfillments.get(&requirement.spec().to_string());
PackageFulfillmentReport {
spec: requirement.spec().clone(),
tree_identity: requirement.tree_identity(),
embedded: requirement.is_embedded(),
provenance: supplied.and_then(|value| value.provenance.clone()),
cache_hit: supplied.is_some_and(|value| value.cache_hit),
}
})
.collect(),
fonts: pack
.font_requirements()
.iter()
.map(|requirement| {
let supplied = font_fulfillments.get(&requirement.container_identity());
FontFulfillmentReport {
container_identity: requirement.container_identity(),
embedded: requirement.is_embedded(),
provenance: supplied.and_then(|value| value.provenance.clone()),
licensing: supplied.and_then(|value| value.licensing.clone()),
}
})
.collect(),
};
let package_files = package_fulfillments
.into_iter()
.map(|(spec, fulfillment)| (spec, fulfillment.files))
.collect();
let fulfillment_bytes = font_fulfillments
.into_iter()
.map(|(identity, fulfillment)| (identity, fulfillment.data))
.collect();
let dependencies = pack
.materialize_compilation_dependency_snapshot(package_files, &fulfillment_bytes)
.map_err(|error| {
let outcome = match error {
CompilationDependencySnapshotError::Package(error) => package_tree_outcome(*error),
CompilationDependencySnapshotError::Font(error) => font_catalog_outcome(error),
};
PackCompilationPreparationError::Operation {
outcome,
request_inventory: Box::new(request_inventory.clone()),
compilation_identity,
fulfillments: Box::new(fulfillments.clone()),
}
})?;
let world = PackWorld::new(
pack,
dependencies,
overrides.value.replacements,
raw_inputs,
request_inventory
.features
.iter()
.map(|feature| feature.value)
.collect(),
request_inventory.document_time.value,
)
.expect("preflighted Pack World inputs must remain valid");
Ok((
world,
PreparedPackCompilationKernel {
request_inventory,
compilation_identity,
engine_identity,
exporter_identity,
page_selection_implies_untagged_pdf,
fulfillments,
},
))
}
pub(crate) fn compile_pack_kernel(
world: &PackWorld,
kernel: PreparedPackCompilationKernel,
) -> PackCompilationExecution {
let traced = WorldTrace::new(world);
let compiled = compile_with_default_pdf_timestamp(
&traced,
kernel.request_inventory.output_specification.value(),
|| None,
);
let access_trace = traced.snapshot();
match compiled {
Ok(output) => {
#[cfg(feature = "diagnostics")]
let warnings = output.warnings.clone();
#[cfg(feature = "diagnostics")]
let mut presentation_pack_warnings = output.pack_warnings.clone();
#[cfg(feature = "diagnostics")]
if kernel.page_selection_implies_untagged_pdf {
presentation_pack_warnings.push(page_selection_pdf_tags_warning());
}
let diagnostics = project_diagnostics(
&traced,
output.warnings,
DiagnosticPhase::Compilation,
DiagnosticProducer::Engine(kernel.engine_identity),
);
let pack_warnings = project_pack_warnings(
output.pack_warnings,
kernel.page_selection_implies_untagged_pdf,
);
PackCompilationExecution {
result: assemble_compilation_result(
&kernel,
CompilationStatus::Succeeded,
output.artifacts,
diagnostics,
pack_warnings,
output.source_page_count,
access_trace,
),
#[cfg(feature = "diagnostics")]
presentation: PackCompilationPresentation::Succeeded {
warnings,
pack_warnings: presentation_pack_warnings,
},
fulfillments: kernel.fulfillments,
}
}
Err(CompileError::Diagnostics {
errors,
warnings,
pack_warnings,
phase,
source_page_count,
}) => {
#[cfg(feature = "diagnostics")]
let mut presentation_pack_warnings = pack_warnings.clone();
#[cfg(feature = "diagnostics")]
if kernel.page_selection_implies_untagged_pdf {
presentation_pack_warnings.push(page_selection_pdf_tags_warning());
}
#[cfg(feature = "diagnostics")]
let presentation = PackCompilationPresentation::Diagnostics {
errors: errors.clone(),
warnings: warnings.clone(),
pack_warnings: presentation_pack_warnings,
};
let mut diagnostics = project_diagnostics(
&traced,
warnings,
DiagnosticPhase::Compilation,
DiagnosticProducer::Engine(kernel.engine_identity),
);
let producer = match phase {
DiagnosticPhase::Compilation => DiagnosticProducer::Engine(kernel.engine_identity),
DiagnosticPhase::Export => DiagnosticProducer::Exporter(kernel.exporter_identity),
};
diagnostics.extend(project_diagnostics(&traced, errors, phase, producer));
PackCompilationExecution {
result: assemble_compilation_result(
&kernel,
CompilationStatus::Rejected,
vec![],
diagnostics,
project_pack_warnings(
pack_warnings,
kernel.page_selection_implies_untagged_pdf,
),
source_page_count,
access_trace,
),
#[cfg(feature = "diagnostics")]
presentation,
fulfillments: kernel.fulfillments,
}
}
Err(CompileError::PngExport {
message,
warnings,
pack_warnings,
source_page_count,
source_page_number,
}) => {
#[cfg(feature = "diagnostics")]
let mut presentation_pack_warnings = pack_warnings.clone();
#[cfg(feature = "diagnostics")]
if kernel.page_selection_implies_untagged_pdf {
presentation_pack_warnings.push(page_selection_pdf_tags_warning());
}
#[cfg(feature = "diagnostics")]
let presentation = PackCompilationPresentation::PngExport {
error: format!("PNG export failed for source page {source_page_number}: {message}"),
warnings: warnings.clone(),
pack_warnings: presentation_pack_warnings,
};
let mut diagnostics = project_diagnostics(
&traced,
warnings,
DiagnosticPhase::Compilation,
DiagnosticProducer::Engine(kernel.engine_identity),
);
diagnostics.push(CompilationDiagnostic {
severity: DiagnosticSeverity::Error,
message,
span: LogicalSpan {
logical_path: None,
byte_range: None,
},
hints: vec![],
trace: vec![],
phase: DiagnosticPhase::Export,
producer: DiagnosticProducer::Exporter(kernel.exporter_identity),
source_page_number: Some(source_page_number),
});
PackCompilationExecution {
result: assemble_compilation_result(
&kernel,
CompilationStatus::Rejected,
vec![],
diagnostics,
project_pack_warnings(
pack_warnings,
kernel.page_selection_implies_untagged_pdf,
),
Some(source_page_count),
access_trace,
),
#[cfg(feature = "diagnostics")]
presentation,
fulfillments: kernel.fulfillments,
}
}
Err(CompileError::InvalidPdfStandards(error)) => {
unreachable!("PDF standards are validated during request preparation: {error}");
}
}
}
fn assemble_compilation_result(
kernel: &PreparedPackCompilationKernel,
status: CompilationStatus,
artifacts: Vec<CompilationArtifact>,
diagnostics: Vec<CompilationDiagnostic>,
pack_warnings: Vec<PackCompilationWarning>,
source_page_count: Option<usize>,
access_trace: CompilationAccessTrace,
) -> CompilationResult {
finalize_result(CompilationResult {
status,
artifacts,
diagnostics,
pack_warnings,
document: document_summary(
kernel.request_inventory.output_specification.value(),
source_page_count,
),
access_trace,
result_identity: CompilationResultIdentity(0),
request_inventory: kernel.request_inventory.clone(),
compilation_identity: kernel.compilation_identity,
engine_identity: kernel.engine_identity,
exporter_identity: kernel.exporter_identity,
})
}
fn document_summary(
output: &CompilationOutputSpecification,
source_page_count: Option<usize>,
) -> CompilationDocumentSummary {
CompilationDocumentSummary {
target: output.target(),
source_page_count,
}
}
fn finalize_result(mut result: CompilationResult) -> CompilationResult {
let artifacts = result
.artifacts
.iter()
.map(|artifact| {
(
artifact.format,
artifact.source_page_number,
artifact.bytes.len(),
typst::utils::hash128(&artifact.bytes),
)
})
.collect::<Vec<_>>();
result.result_identity = CompilationResultIdentity(typst::utils::hash128(&(
"typst-pack-compilation-result-v1",
result.compilation_identity,
result.status,
result.document,
&result.diagnostics,
&result.pack_warnings,
&result.access_trace,
artifacts,
)));
result
}
pub(crate) fn package_tree_outcome(error: PackageTreeError) -> CompilationOperationOutcome {
match error {
PackageTreeError::Missing { packages } => {
CompilationOperationOutcome::MissingExternalPackageFulfillment { packages }
}
PackageTreeError::Mismatched {
spec,
expected,
actual,
expected_file_count,
actual_file_count,
expected_byte_length,
actual_byte_length,
} => CompilationOperationOutcome::MismatchedExternalPackageTree {
spec,
expected,
actual,
expected_file_count,
actual_file_count,
expected_byte_length,
actual_byte_length,
},
PackageTreeError::Malformed {
spec,
path,
message,
} => CompilationOperationOutcome::MalformedExternalPackageTree {
spec,
path,
message,
},
}
}
fn font_catalog_outcome(error: FontCatalogError) -> CompilationOperationOutcome {
match error {
FontCatalogError::Missing { containers } => {
CompilationOperationOutcome::MissingExternalFontFulfillment { containers }
}
FontCatalogError::Mismatched {
expected,
actual,
expected_length,
actual_length,
} => CompilationOperationOutcome::MismatchedExternalFontContainer {
expected,
actual,
expected_length,
actual_length,
},
FontCatalogError::Malformed { container, index } => {
CompilationOperationOutcome::MalformedExternalFontContainer { container, index }
}
}
}
fn compilation_identity(
pack: &Pack,
inventory: &CompilationRequestInventory,
engine_identity: EngineIdentity,
exporter_identity: ExporterIdentity,
) -> CompilationIdentity {
let output_digest = match inventory.output_specification.value() {
CompilationOutputSpecification::Pdf(specification) => {
let page_selection = canonical_page_selection(&specification.page_selection);
let mut standards = specification
.standards
.iter()
.map(pdf_standard_identity)
.collect::<Vec<_>>();
standards.sort_unstable();
typst::utils::hash128(&(
"pdf",
&page_selection,
&specification.identifier,
&specification.creator,
specification.tags,
specification.creation_timestamp,
standards,
specification.pretty,
))
}
CompilationOutputSpecification::Png(specification) => {
let page_selection = canonical_page_selection(&specification.page_selection);
typst::utils::hash128(&(
"png",
&page_selection,
specification.pixels_per_inch.map(f64::to_bits),
specification.render_bleed,
))
}
CompilationOutputSpecification::Svg(specification) => {
let page_selection = canonical_page_selection(&specification.page_selection);
typst::utils::hash128(&(
"svg",
&page_selection,
specification.render_bleed,
specification.pretty,
))
}
CompilationOutputSpecification::Html(specification) => {
typst::utils::hash128(&("html", specification.pretty))
}
};
let feature_values = inventory
.features
.iter()
.map(|feature| feature.value)
.collect::<Vec<_>>();
let (document_time, document_timestamp) = inventory.document_time.value.identity_projection();
CompilationIdentity(typst::utils::hash128(&(
"typst-pack-compilation-v1",
pack.identity(),
inventory.output_specification.value().format(),
output_digest,
inventory.inputs.value.commitment,
inventory
.overrides
.value
.iter()
.map(|entry| (&entry.path, entry.byte_len, entry.commitment))
.collect::<Vec<_>>(),
feature_values,
document_time,
document_timestamp,
engine_identity,
exporter_identity,
)))
}
fn canonical_page_selection(selection: &PageSelection) -> (bool, Vec<(usize, usize)>) {
let selects_all = selection.ranges.is_empty();
let mut ranges = selection
.ranges
.iter()
.filter_map(|range| {
let start = range.start().map_or(1, NonZeroUsize::get);
let end = range.end().map_or(usize::MAX, NonZeroUsize::get);
(start <= end).then_some((start, end))
})
.collect::<Vec<_>>();
ranges.sort_unstable();
let mut canonical: Vec<(usize, usize)> = vec![];
for (start, end) in ranges {
if let Some(last) = canonical.last_mut()
&& start <= last.1.saturating_add(1)
{
last.1 = last.1.max(end);
} else {
canonical.push((start, end));
}
}
(selects_all, canonical)
}
fn pdf_standard_identity(standard: &PdfStandard) -> &'static str {
match standard {
PdfStandard::V_1_4 => "1.4",
PdfStandard::V_1_5 => "1.5",
PdfStandard::V_1_6 => "1.6",
PdfStandard::V_1_7 => "1.7",
PdfStandard::V_2_0 => "2.0",
PdfStandard::A_1b => "a-1b",
PdfStandard::A_1a => "a-1a",
PdfStandard::A_2b => "a-2b",
PdfStandard::A_2u => "a-2u",
PdfStandard::A_2a => "a-2a",
PdfStandard::A_3b => "a-3b",
PdfStandard::A_3u => "a-3u",
PdfStandard::A_3a => "a-3a",
PdfStandard::A_4 => "a-4",
PdfStandard::A_4f => "a-4f",
PdfStandard::A_4e => "a-4e",
PdfStandard::Ua_1 => "ua-1",
_ => unreachable!("all standards in pinned typst-pdf are represented"),
}
}
pub(crate) fn compile_with_default_pdf_timestamp(
world: &dyn World,
specification: &CompilationOutputSpecification,
default_pdf_timestamp: impl FnOnce() -> Option<Timestamp>,
) -> Result<CompilationOutput, CompileError> {
let _compilation_timing = typst_timing::TimingScope::new("typst-pack compilation");
if let CompilationOutputSpecification::Html(specification) = specification {
let pack_warnings = EcoVec::new();
let Warned { output, warnings } = EmbeddedTypst::compile_html(world);
let document = output.map_err(|errors| CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
pack_warnings: pack_warnings.clone(),
phase: DiagnosticPhase::Compilation,
source_page_count: None,
})?;
let _export_timing = typst_timing::TimingScope::new("export");
let bytes = EmbeddedTypst::export_html(
&document,
&typst_html::HtmlOptions {
pretty: specification.pretty,
},
)
.map_err(|errors| CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
pack_warnings: pack_warnings.clone(),
phase: DiagnosticPhase::Export,
source_page_count: None,
})?;
return Ok(CompilationOutput {
artifacts: vec![CompilationArtifact {
format: OutputFormat::Html,
bytes,
source_page_number: None,
}],
warnings,
pack_warnings,
source_page_count: None,
});
}
let Warned {
output,
warnings: compile_warnings,
} = EmbeddedTypst::compile_paged(world);
let warnings = compile_warnings;
let mut pack_warnings = EcoVec::new();
if let CompilationOutputSpecification::Pdf(specification) = specification
&& !specification.page_selection.ranges().is_empty()
&& specification.tags.is_auto()
&& PdfOptions::default().tagged
{
pack_warnings.push(page_selection_pdf_tags_warning());
}
let document = output.map_err(|errors| CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
pack_warnings: pack_warnings.clone(),
phase: DiagnosticPhase::Compilation,
source_page_count: None,
})?;
let source_page_count = document.pages().len();
let artifacts = {
let _export_timing = typst_timing::TimingScope::new("export");
match specification {
CompilationOutputSpecification::Pdf(specification) => {
let standards = validate_pdf_standards(&specification.standards)
.map_err(CompileError::InvalidPdfStandards)?;
let timestamp = match specification.creation_timestamp {
CreationTimestamp::Automatic => default_pdf_timestamp(),
CreationTimestamp::Explicit(timestamp) => Some(timestamp),
CreationTimestamp::Omit => None,
};
let pdf_options = PdfOptions {
ident: specification.identifier.clone(),
creator: specification.creator.clone(),
timestamp,
page_ranges: specification.page_selection.typst_page_ranges(),
standards,
tagged: match specification.tags {
Smart::Auto => {
PdfOptions::default().tagged
&& specification.page_selection.ranges().is_empty()
}
Smart::Custom(tagged) => tagged,
},
pretty: specification.pretty,
};
let pdf = EmbeddedTypst::export_pdf(&document, &pdf_options).map_err(|errors| {
CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
pack_warnings: pack_warnings.clone(),
phase: DiagnosticPhase::Export,
source_page_count: Some(source_page_count),
}
})?;
vec![CompilationArtifact {
format: OutputFormat::Pdf,
bytes: pdf,
source_page_number: None,
}]
}
CompilationOutputSpecification::Png(specification) => {
let pixels_per_inch = specification
.pixels_per_inch
.unwrap_or_else(default_png_ppi);
let render_options = typst_render::RenderOptions {
pixel_per_pt: (pixels_per_inch / 72.0).into(),
render_bleed: specification.render_bleed,
};
let pages =
selected_pages(&document, &specification.page_selection).collect::<Vec<_>>();
let export = |(source_page_number, page)| {
let bytes =
EmbeddedTypst::export_png(page, &render_options).map_err(|message| {
CompileError::PngExport {
message,
warnings: warnings.clone(),
pack_warnings: pack_warnings.clone(),
source_page_count,
source_page_number,
}
})?;
Ok::<_, CompileError>(CompilationArtifact {
format: OutputFormat::Png,
bytes,
source_page_number: Some(source_page_number),
})
};
#[cfg(feature = "parallel")]
let artifacts = pages
.into_par_iter()
.map(export)
.collect::<Result<Vec<_>, _>>()?;
#[cfg(not(feature = "parallel"))]
let artifacts = pages
.into_iter()
.map(export)
.collect::<Result<Vec<_>, _>>()?;
artifacts
}
CompilationOutputSpecification::Svg(specification) => {
let svg_options = typst_svg::SvgOptions {
render_bleed: specification.render_bleed,
pretty: specification.pretty,
};
let pages =
selected_pages(&document, &specification.page_selection).collect::<Vec<_>>();
let export = |(source_page_number, page)| CompilationArtifact {
format: OutputFormat::Svg,
bytes: EmbeddedTypst::export_svg(page, &svg_options),
source_page_number: Some(source_page_number),
};
#[cfg(feature = "parallel")]
let artifacts = pages.into_par_iter().map(export).collect();
#[cfg(not(feature = "parallel"))]
let artifacts = pages.into_iter().map(export).collect();
artifacts
}
CompilationOutputSpecification::Html(_) => unreachable!("handled above"),
}
};
Ok(CompilationOutput {
artifacts,
warnings,
pack_warnings,
source_page_count: Some(source_page_count),
})
}
pub(crate) fn validate_pdf_standards(
standards: &[PdfStandard],
) -> Result<PdfStandards, PdfStandardsValidationError> {
PdfStandards::new(standards).map_err(|error| PdfStandardsValidationError {
message: error.message().to_string(),
hints: error.hints().iter().map(ToString::to_string).collect(),
})
}
#[cfg(feature = "diagnostics")]
pub(crate) fn pdf_standard_requiring_tags(standards: &[PdfStandard]) -> Option<&'static str> {
standards.iter().find_map(|standard| match standard {
PdfStandard::A_1a => Some("PDF/A-1a"),
PdfStandard::A_2a => Some("PDF/A-2a"),
PdfStandard::A_3a => Some("PDF/A-3a"),
PdfStandard::Ua_1 => Some("PDF/UA-1"),
_ => None,
})
}
fn selected_pages<'a>(
document: &'a PagedDocument,
page_selection: &'a PageSelection,
) -> impl Iterator<Item = (NonZeroUsize, &'a typst_layout::Page)> {
let ranges = page_selection.typst_page_ranges();
document
.pages()
.iter()
.enumerate()
.filter(move |(index, _)| {
ranges.as_ref().is_none_or(|ranges| {
NonZeroUsize::new(index + 1).is_some_and(|number| ranges.includes_page(number))
})
})
.map(|(index, page)| (NonZeroUsize::new(index + 1).unwrap(), page))
}
fn default_png_ppi() -> f64 {
typst_render::RenderOptions::default().pixel_per_pt.get() * 72.0
}
fn project_diagnostics(
world: &dyn World,
diagnostics: impl IntoIterator<Item = SourceDiagnostic>,
phase: DiagnosticPhase,
producer: DiagnosticProducer,
) -> Vec<CompilationDiagnostic> {
diagnostics
.into_iter()
.map(|diagnostic| CompilationDiagnostic {
severity: match diagnostic.severity {
Severity::Error => DiagnosticSeverity::Error,
Severity::Warning => DiagnosticSeverity::Warning,
},
message: diagnostic.message.into(),
span: logical_span(world, diagnostic.span),
hints: diagnostic
.hints
.into_iter()
.map(|hint| DiagnosticHint {
message: hint.v.into(),
span: logical_span(world, hint.span),
})
.collect(),
trace: diagnostic
.trace
.into_iter()
.map(|trace| {
let (kind, value) = match trace.v {
Tracepoint::Call(value) => (TracepointKind::Call, value.map(String::from)),
Tracepoint::Show(value) => (TracepointKind::Show, Some(value.into())),
Tracepoint::Import(value) => (TracepointKind::Import, Some(value.into())),
Tracepoint::Include(value) => (TracepointKind::Include, Some(value.into())),
};
DiagnosticTracepoint {
kind,
value,
span: logical_span(world, trace.span.into()),
}
})
.collect(),
phase,
producer,
source_page_number: None,
})
.collect()
}
fn project_pack_warnings(
warnings: impl IntoIterator<Item = SourceDiagnostic>,
page_selection_implies_untagged_pdf: bool,
) -> Vec<PackCompilationWarning> {
warnings
.into_iter()
.chain(page_selection_implies_untagged_pdf.then(page_selection_pdf_tags_warning))
.map(|warning| PackCompilationWarning {
message: warning.message.into(),
hints: warning
.hints
.into_iter()
.map(|hint| hint.v.into())
.collect(),
})
.collect()
}
fn page_selection_pdf_tags_warning() -> SourceDiagnostic {
SourceDiagnostic::warning(Span::detached(), "using --pages implies --no-pdf-tags").with_hints([
"the resulting PDF will be inaccessible".into(),
"add --no-pdf-tags to silence this warning".into(),
])
}
fn logical_span(world: &dyn World, span: DiagSpan) -> LogicalSpan {
LogicalSpan {
logical_path: span.id().map(logical_path),
byte_range: world.range(span),
}
}
#[cfg(test)]
mod result_identity_tests {
use super::*;
#[test]
fn compilation_trace_retains_missing_font_requests() {
let trace = CompilationAccessTrace::from_observations(BTreeSet::from([
CompilationAccessObservation::new(
CompilationAccessKind::Font,
"font-index:7".to_owned(),
Some(7),
CompilationAccessOutcome::Missing,
),
]));
let observation = trace.observations().next().unwrap();
assert_eq!(observation.kind(), CompilationAccessKind::Font);
assert_eq!(observation.logical_path(), "font-index:7");
assert_eq!(observation.font_index(), Some(7));
assert_eq!(observation.outcome(), &CompilationAccessOutcome::Missing);
}
#[test]
fn result_identity_binds_each_post_execution_projection() {
let pack = Pack::builder("main.typ")
.file(
"main.typ",
b"#set page(width: 20pt, height: 10pt, margin: 0pt)\n#rect(width: 1pt, height: 1pt)".to_vec(),
)
.unwrap()
.build()
.unwrap();
let report = compile(PackCompilationRequest::new(
pack,
CompilationOutputSpecification::Svg(SvgOutputSpecification::default()),
))
.unwrap();
let base = report.result().unwrap().clone();
let identity = base.result_identity;
let mut status = base.clone();
status.status = CompilationStatus::Rejected;
assert_ne!(finalize_result(status).result_identity, identity);
let mut document = base.clone();
document.document.source_page_count = Some(2);
assert_ne!(finalize_result(document).result_identity, identity);
let mut diagnostics = base.clone();
diagnostics.diagnostics.push(CompilationDiagnostic {
severity: DiagnosticSeverity::Warning,
message: "identity warning".to_owned(),
span: LogicalSpan {
logical_path: None,
byte_range: None,
},
hints: vec![],
trace: vec![],
phase: DiagnosticPhase::Compilation,
producer: DiagnosticProducer::Engine(base.engine_identity),
source_page_number: None,
});
assert_ne!(finalize_result(diagnostics).result_identity, identity);
let mut access = base.clone();
access
.access_trace
.observations
.insert(CompilationAccessObservation {
kind: CompilationAccessKind::File,
logical_path: "project:missing.txt".to_owned(),
font_index: None,
outcome: CompilationAccessOutcome::Missing,
});
assert_ne!(finalize_result(access).result_identity, identity);
let mut artifact = base;
artifact.artifacts[0].bytes.push(0);
assert_ne!(finalize_result(artifact).result_identity, identity);
}
}