diff --git a/Cargo.lock b/Cargo.lock index c51493f6a..7b10a3abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3095,6 +3095,7 @@ dependencies = [ "image", "krilla", "krilla-svg", + "serde", "typst-assets", "typst-library", "typst-macros", diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index b29b2f076..c070bc800 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -241,10 +241,6 @@ pub struct CompileArgs { #[arg(long = "pages", value_delimiter = ',')] pub pages: Option>, - /// The version of the produced PDF. - #[arg(long = "pdf-version")] - pub pdf_version: Option, - /// One (or multiple comma-separated) PDF standards that Typst will enforce /// conformance with. #[arg(long = "pdf-standard", value_delimiter = ',')] @@ -467,10 +463,10 @@ pub enum Feature { display_possible_values!(Feature); -/// A PDF version. +/// A PDF standard that Typst can enforce conformance with. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[allow(non_camel_case_types)] -pub enum PdfVersion { +pub enum PdfStandard { /// PDF 1.4. #[value(name = "1.4")] V_1_4, @@ -486,14 +482,6 @@ pub enum PdfVersion { /// PDF 2.0. #[value(name = "2.0")] V_2_0, -} - -display_possible_values!(PdfVersion); - -/// A PDF standard that Typst can enforce conformance with. -#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] -#[allow(non_camel_case_types)] -pub enum PdfStandard { /// PDF/A-1b. #[value(name = "a-1b")] A_1b, @@ -520,6 +508,8 @@ pub enum PdfStandard { A_4e, } +display_possible_values!(PdfStandard); + // Output file format for query command #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, ValueEnum)] pub enum SerializationFormat { diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 1e49f2116..4edb4c323 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -18,11 +18,11 @@ use typst::html::HtmlDocument; use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::syntax::{FileId, Source, Span}; use typst::WorldExt; -use typst_pdf::{PdfOptions, Timestamp, Validator}; +use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; use crate::args::{ CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, - PdfStandard, PdfVersion, WatchCommand, + PdfStandard, WatchCommand, }; #[cfg(feature = "http-server")] use crate::server::HtmlServer; @@ -63,10 +63,8 @@ pub struct CompileConfig { /// Opens the output file with the default viewer or a specific program after /// compilation. pub open: Option>, - /// The version that should be used to export the PDF. - pub pdf_version: Option, /// A list of standards the PDF should conform to. - pub pdf_standard: Vec, + pub pdf_standards: PdfStandards, /// A path to write a Makefile rule describing the current compilation. pub make_deps: Option, /// The PPI (pixels per inch) to use for PNG export. @@ -131,6 +129,10 @@ impl CompileConfig { PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) }); + let pdf_standards = PdfStandards::new( + &args.pdf_standard.iter().copied().map(Into::into).collect::>(), + )?; + #[cfg(feature = "http-server")] let server = match watch { Some(command) @@ -147,8 +149,7 @@ impl CompileConfig { output, output_format, pages, - pdf_version: args.pdf_version, - pdf_standard: args.pdf_standard.clone(), + pdf_standards, creation_timestamp: args.world.creation_timestamp, make_deps: args.make_deps.clone(), ppi: args.ppi, @@ -285,44 +286,11 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult< } }; - let validator = match config.pdf_standard.first() { - None => None, - Some(s) => { - let validator = if config.pdf_standard.len() > 1 { - bail!(Span::detached(), "cannot export using more than one PDF standard"; - hint: "typst currently only supports export using \ - one standard at the same time"); - } else { - match s { - PdfStandard::A_1b => Validator::A_1b, - PdfStandard::A_2b => Validator::A_2b, - PdfStandard::A_2u => Validator::A_2u, - PdfStandard::A_3b => Validator::A_3b, - PdfStandard::A_3u => Validator::A_3u, - PdfStandard::A_4 => Validator::A_4, - PdfStandard::A_4f => Validator::A_4f, - PdfStandard::A_4e => Validator::A_4e, - } - }; - - Some(validator) - } - }; - - let pdf_version = config.pdf_version.map(|v| match v { - PdfVersion::V_1_4 => typst_pdf::PdfVersion::Pdf14, - PdfVersion::V_1_5 => typst_pdf::PdfVersion::Pdf15, - PdfVersion::V_1_6 => typst_pdf::PdfVersion::Pdf16, - PdfVersion::V_1_7 => typst_pdf::PdfVersion::Pdf17, - PdfVersion::V_2_0 => typst_pdf::PdfVersion::Pdf20, - }); - let options = PdfOptions { ident: Smart::Auto, timestamp, page_ranges: config.pages.clone(), - validator, - pdf_version, + standards: config.pdf_standards.clone(), }; let buffer = typst_pdf::pdf(document, &options)?; config @@ -788,3 +756,23 @@ impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { }) } } + +impl From for typst_pdf::PdfStandard { + fn from(standard: PdfStandard) -> Self { + match standard { + PdfStandard::V_1_4 => typst_pdf::PdfStandard::V_1_4, + PdfStandard::V_1_5 => typst_pdf::PdfStandard::V_1_5, + PdfStandard::V_1_6 => typst_pdf::PdfStandard::V_1_6, + PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, + PdfStandard::V_2_0 => typst_pdf::PdfStandard::V_2_0, + PdfStandard::A_1b => typst_pdf::PdfStandard::A_1b, + PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + PdfStandard::A_2u => typst_pdf::PdfStandard::A_2u, + PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, + PdfStandard::A_3u => typst_pdf::PdfStandard::A_3u, + PdfStandard::A_4 => typst_pdf::PdfStandard::A_4, + PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f, + PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e, + } + } +} diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index 3deda2cc8..f6f08b5bc 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -25,6 +25,7 @@ ecow = { workspace = true } image = { workspace = true } krilla = { workspace = true } krilla-svg = { workspace = true } +serde = { workspace = true } [lints] workspace = true diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 5a35c2519..0db7b8855 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -38,15 +38,13 @@ pub fn convert( typst_document: &PagedDocument, options: &PdfOptions, ) -> SourceResult> { - let configuration = get_configuration(options)?; - let settings = SerializeSettings { compress_content_streams: true, no_device_cs: true, ascii_compatible: false, xmp_metadata: true, cmyk_profile: None, - configuration, + configuration: options.standards.config, enable_tagging: false, render_svg_glyph_fn: render_svg_glyph, }; @@ -68,7 +66,7 @@ pub fn convert( document.set_outline(build_outline(&gc)); document.set_metadata(build_metadata(&gc)); - finish(document, gc, configuration) + finish(document, gc, options.standards.config) } fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResult<()> { @@ -606,37 +604,6 @@ fn collect_named_destinations( locs_to_names } -fn get_configuration(options: &PdfOptions) -> SourceResult { - let config = match (options.pdf_version, options.validator) { - (None, None) => { - Configuration::new_with_version(krilla::configure::PdfVersion::Pdf17) - } - (Some(pdf), None) => Configuration::new_with_version(pdf.into()), - (None, Some(v)) => Configuration::new_with_validator(v.into()), - (Some(pdf), Some(v)) => { - let pdf = pdf.into(); - let v = v.into(); - - match Configuration::new_with(v, pdf) { - Some(c) => c, - None => { - let pdf_string = pdf.as_str(); - let s_string = v.as_str(); - - let h_message = format!( - "export using version {} instead", - v.recommended_version().as_str() - ); - - bail!(Span::detached(), "{pdf_string} is not compatible with {s_string}"; hint: "{h_message}"); - } - } - } - }; - - Ok(config) -} - pub(crate) struct PageIndexConverter { page_indices: HashMap, skipped_pages: usize, diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 171a05786..c7bae57ac 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -12,7 +12,11 @@ mod shape; mod text; mod util; -use typst_library::diag::SourceResult; +use std::fmt::{self, Debug, Formatter}; + +use ecow::eco_format; +use serde::{Deserialize, Serialize}; +use typst_library::diag::{bail, SourceResult, StrResult}; use typst_library::foundations::{Datetime, Smart}; use typst_library::layout::{PageRanges, PagedDocument}; @@ -24,73 +28,6 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult for krilla::configure::PdfVersion { - fn from(value: PdfVersion) -> Self { - match value { - PdfVersion::Pdf14 => krilla::configure::PdfVersion::Pdf14, - PdfVersion::Pdf15 => krilla::configure::PdfVersion::Pdf15, - PdfVersion::Pdf16 => krilla::configure::PdfVersion::Pdf16, - PdfVersion::Pdf17 => krilla::configure::PdfVersion::Pdf17, - PdfVersion::Pdf20 => krilla::configure::PdfVersion::Pdf20, - } - } -} - -/// A validator for exporting PDF documents to a specific subset of PDF. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[allow(non_camel_case_types)] -pub enum Validator { - /// The validator for the PDF/A1-A standard. - A1_A, - /// The validator for the PDF/A1-B standard. - A_1b, - /// The validator for the PDF/A2-B standard. - A_2b, - /// The validator for the PDF/A2-U standard. - A_2u, - /// The validator for the PDF/A3-B standard. - A_3b, - /// The validator for the PDF/A3-U standard. - A_3u, - /// The validator for the PDF/A4 standard. - A_4, - /// The validator for the PDF/A4f standard. - A_4f, - /// The validator for the PDF/A4e standard. - A_4e, -} - -impl From for krilla::configure::Validator { - fn from(value: Validator) -> Self { - match value { - Validator::A1_A => krilla::configure::Validator::A1_A, - Validator::A_1b => krilla::configure::Validator::A1_B, - Validator::A_2b => krilla::configure::Validator::A2_B, - Validator::A_2u => krilla::configure::Validator::A2_U, - Validator::A_3b => krilla::configure::Validator::A3_B, - Validator::A_3u => krilla::configure::Validator::A3_U, - Validator::A_4 => krilla::configure::Validator::A4, - Validator::A_4f => krilla::configure::Validator::A4F, - Validator::A_4e => krilla::configure::Validator::A4E, - } - } -} - /// Settings for PDF export. #[derive(Debug, Default)] pub struct PdfOptions<'a> { @@ -112,10 +49,138 @@ pub struct PdfOptions<'a> { /// Specifies which ranges of pages should be exported in the PDF. When /// `None`, all pages should be exported. pub page_ranges: Option, - /// The version that should be used to export the PDF. - pub pdf_version: Option, - /// A standard the PDF should conform to. - pub validator: Option, + /// A list of PDF standards that Typst will enforce conformance with. + pub standards: PdfStandards, +} + +/// Encapsulates a list of compatible PDF standards. +#[derive(Clone)] +pub struct PdfStandards { + pub(crate) config: krilla::configure::Configuration, +} + +impl PdfStandards { + /// Validates a list of PDF standards for compatibility and returns their + /// encapsulated representation. + pub fn new(list: &[PdfStandard]) -> StrResult { + use krilla::configure::{Configuration, PdfVersion, Validator}; + + let mut version: Option = None; + let mut set_version = |v: PdfVersion| -> StrResult<()> { + if let Some(prev) = version { + bail!( + "PDF cannot conform to {} and {} at the same time", + prev.as_str(), + v.as_str() + ); + } + version = Some(v); + Ok(()) + }; + + let mut validator = None; + let mut set_validator = |v: Validator| -> StrResult<()> { + if validator.is_some() { + bail!("Typst currently only supports one PDF substandard at a time"); + } + validator = Some(v); + Ok(()) + }; + + for standard in list { + match standard { + PdfStandard::V_1_4 => set_version(PdfVersion::Pdf14)?, + PdfStandard::V_1_5 => set_version(PdfVersion::Pdf15)?, + PdfStandard::V_1_6 => set_version(PdfVersion::Pdf16)?, + PdfStandard::V_1_7 => set_version(PdfVersion::Pdf17)?, + PdfStandard::V_2_0 => set_version(PdfVersion::Pdf20)?, + PdfStandard::A_1b => set_validator(Validator::A1_B)?, + PdfStandard::A_2b => set_validator(Validator::A2_B)?, + PdfStandard::A_2u => set_validator(Validator::A2_U)?, + PdfStandard::A_3b => set_validator(Validator::A3_B)?, + PdfStandard::A_3u => set_validator(Validator::A3_U)?, + PdfStandard::A_4 => set_validator(Validator::A4)?, + PdfStandard::A_4f => set_validator(Validator::A4F)?, + PdfStandard::A_4e => set_validator(Validator::A4E)?, + } + } + + let version = version.unwrap_or(PdfVersion::Pdf17); + let validator = validator.unwrap_or_default(); + + let config = Configuration::new_with(validator, version).ok_or_else(|| { + eco_format!( + "{} is not compatible with {}", + version.as_str(), + validator.as_str() + ) + })?; + + Ok(Self { config }) + } +} + +impl Debug for PdfStandards { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.pad("PdfStandards(..)") + } +} + +impl Default for PdfStandards { + fn default() -> Self { + use krilla::configure::{Configuration, PdfVersion}; + Self { + config: Configuration::new_with_version(PdfVersion::Pdf17), + } + } +} + +/// 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.4. + #[serde(rename = "1.4")] + V_1_4, + /// PDF 1.5. + #[serde(rename = "1.5")] + V_1_5, + /// PDF 1.5. + #[serde(rename = "1.6")] + V_1_6, + /// PDF 1.7. + #[serde(rename = "1.7")] + V_1_7, + /// PDF 2.0. + #[serde(rename = "2.0")] + V_2_0, + /// PDF/A-1b. + #[serde(rename = "a-1b")] + A_1b, + /// PDF/A-2b. + #[serde(rename = "a-2b")] + A_2b, + /// PDF/A-2u. + #[serde(rename = "a-2u")] + A_2u, + /// PDF/A-3u. + #[serde(rename = "a-3b")] + A_3b, + /// PDF/A-3u. + #[serde(rename = "a-3u")] + A_3u, + /// PDF/A-4. + #[serde(rename = "a-4")] + A_4, + /// PDF/A-4f. + #[serde(rename = "a-4f")] + A_4f, + /// PDF/A-4e. + #[serde(rename = "a-4e")] + A_4e, } /// A timestamp with timezone information.