Repository: typst/typst
File: crates/typst-pdf/src/lib.rs (Rust)

Code before:
    }
}

/// A PDF standard that Typst can enforce conformance with.
///
/// Support for more standards is planned.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
#[non_exhaustive]
pub enum PdfStandard {
    /// PDF 1.7.
    #[serde(rename = "1.7")]
    V_1_7,
    /// PDF/A-2b.
    #[serde(rename = "a-2b")]
    A_2b,
    /// PDF/A-3b.
    #[serde(rename = "a-3b")]
    A_3b,
}

/// Conformance levels of PDF/A
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
#[non_exhaustive]
pub enum PdfAConformanceLevel {
    /// PDF/A-2
    A_2,
    /// PDF/A-3
    A_3,
}

impl From<PdfAConformanceLevel> for i32 {
    fn from(value: PdfAConformanceLevel) -> Self {
        match value {
            PdfAConformanceLevel::A_2 => 2,
            PdfAConformanceLevel::A_3 => 3,
        }
    }
}

/// A struct to build a PDF following a fixed succession of phases.
///
/// This type uses generics to represent its current state. `S` (for "state") is
/// all data that was produced by the previous phases, that is now read-only.
///
/// Phase after phase, this state will be transformed. Each phase corresponds to
/// a call to the [eponymous function](`PdfBuilder::phase`) and produces a new
/// part of the state, that will be aggregated with all other information, for
/// consumption during the next phase.
///

Review comment (suggestion):
This should not be public. We intentionally do not provide such an enum to keep the API flexible. Internally, we can turn `&[PdfStandard]` into whatever is most convenient at the time, rejecting bad combinations.

I think as is the enum is a bit strange because it drops the "b".

As various checks are required by different standards (e.g. all your `is_some()`) checks, another design might be to not store level enums, but rather a bunch of booleans specifying which checks should be performed. 

If we like this bool-based design, maybe for now we could realize it with just `pdfa: bool, embedded_files: bool` and later break out the `pdfa: bool` into separate ones. And for the XMP metadata we could just store a field `(u8, &'static str)` (e.g. `(2, "B")`).

Code after:
/// A timezone.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Timezone {
    /// The UTC timezone.
    UTC,
    /// The local timezone offset from UTC. And the `minute_offset` will have
    /// same sign as `hour_offset`.
    Local { hour_offset: i8, minute_offset: u8 },
}

/// Encapsulates a list of compatible PDF standards.
#[derive(Clone)]
pub struct PdfStandards {
    /// For now, we simplify to just PDF/A. But it can be more fine-grained in
    /// the future.
    pub(crate) pdfa: bool,
    /// Whether the standard allows for embedding any kind of file into the PDF.
    /// We disallow this for PDF/A-2, since it only allows embedding
    /// PDF/A-1 and PDF/A-2 documents.
    pub(crate) embedded_files: bool,
    /// Part of the PDF/A standard.
    pub(crate) pdfa_part: Option<(i32, &'static str)>,
}

impl PdfStandards {
    /// Validates a list of PDF standards for compatibility and returns their
    /// encapsulated representation.
    pub fn new(list: &[PdfStandard]) -> StrResult<Self> {
        let a2b = list.contains(&PdfStandard::A_2b);
        let a3b = list.contains(&PdfStandard::A_3b);

        if a2b && a3b {
            bail!("PDF cannot conform to A-2B and A-3B at the same time")
        }

        let pdfa = a2b || a3b;
        Ok(Self {
            pdfa,
            embedded_files: !a2b,
            pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")),
        })
    }
}

impl Debug for PdfStandards {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.pad("PdfStandards(..)")
    }
}

impl Default for PdfStandards {