From 880aa4b8a1588c5e6b7c5a1b5e49d7495c284386 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:22:46 +0100 Subject: [PATCH] Add support for exporting to specific version --- crates/typst-cli/src/args.rs | 45 +++++++++++++--- crates/typst-cli/src/compile.rs | 43 ++++++++------- crates/typst-pdf/src/catalog_old.rs | 81 ++++++++++++++--------------- crates/typst-pdf/src/color_font.rs | 6 +-- crates/typst-pdf/src/color_old.rs | 18 +++---- crates/typst-pdf/src/content_old.rs | 35 ++++++------- crates/typst-pdf/src/krilla.rs | 23 ++++++-- crates/typst-pdf/src/lib.rs | 16 +++--- 8 files changed, 162 insertions(+), 105 deletions(-) diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 83c4c8f9e..e255f6708 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -241,10 +241,14 @@ pub struct CompileArgs { #[arg(long = "pages", value_delimiter = ',')] pub pages: Option>, - /// One (or multiple comma-separated) PDF standards that Typst will enforce + /// The version of the produced PDF. + #[arg(long = "pdf-version")] + pub pdf_version: Option, + + /// A PDF standard that Typst will enforce /// conformance with. - #[arg(long = "pdf-standard", value_delimiter = ',')] - pub pdf_standard: Vec, + #[arg(long = "pdf-standard")] + pub pdf_standard: Option, /// The PPI (pixels per inch) to use for PNG export. #[arg(long = "ppi", default_value_t = 144.0)] @@ -463,16 +467,45 @@ pub enum Feature { display_possible_values!(Feature); +/// A PDF version. +#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] +#[allow(non_camel_case_types)] +pub enum PdfVersion { + /// PDF 1.4. + #[value(name = "1.4")] + V_1_4, + /// PDF 1.5. + #[value(name = "1.5")] + V_1_5, + /// PDF 1.5. + #[value(name = "1.6")] + V_1_6, + /// PDF 1.7. + #[value(name = "1.7")] + V_1_7, +} + +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 1.7. - #[value(name = "1.7")] - V_1_7, + /// PDF/A-2u. + #[value(name = "a-1b")] + A_1b, /// PDF/A-2b. #[value(name = "a-2b")] A_2b, + /// PDF/A-2u. + #[value(name = "a-2u")] + A_2u, + /// PDF/A-3u. + #[value(name = "a-3b")] + A_3b, + /// PDF/A-2b. + #[value(name = "a-3u")] + A_3u, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 3aa3aa3b9..67f339fc4 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -17,11 +17,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, PdfStandards}; +use typst_pdf::{PdfOptions, PdfStandards, Validator}; use crate::args::{ CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, - PdfStandard, WatchCommand, + PdfStandard, PdfVersion, WatchCommand, }; #[cfg(feature = "http-server")] use crate::server::HtmlServer; @@ -62,9 +62,10 @@ pub struct CompileConfig { /// Opens the output file with the default viewer or a specific program after /// compilation. pub open: Option>, - /// One (or multiple comma-separated) PDF standards that Typst will enforce - /// conformance with. - pub pdf_standards: PdfStandards, + /// The version that should be used to export the PDF. + pub pdf_version: Option, + /// A standard the PDF should conform to. + pub pdf_standard: Option, /// 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. @@ -129,18 +130,6 @@ impl CompileConfig { PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) }); - let pdf_standards = { - let list = args - .pdf_standard - .iter() - .map(|standard| match standard { - PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, - PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, - }) - .collect::>(); - PdfStandards::new(&list)? - }; - #[cfg(feature = "http-server")] let server = match watch { Some(command) @@ -157,15 +146,16 @@ impl CompileConfig { output, output_format, pages, - pdf_standards, creation_timestamp: args.world.creation_timestamp, make_deps: args.make_deps.clone(), ppi: args.ppi, diagnostic_format: args.process.diagnostic_format, open: args.open.clone(), + pdf_version: args.pdf_version, export_cache: ExportCache::new(), #[cfg(feature = "http-server")] server, + pdf_standard: args.pdf_standard, }) } } @@ -277,7 +267,22 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult< config.creation_timestamp.unwrap_or_else(chrono::Utc::now), ), page_ranges: config.pages.clone(), - standards: config.pdf_standards.clone(), + 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, + }), + validator: config + .pdf_standard + .map(|s| match s { + PdfStandard::A_1b => Validator::A1_B, + PdfStandard::A_2b => Validator::A2_B, + PdfStandard::A_2u => Validator::A2_U, + PdfStandard::A_3b => Validator::A3_B, + PdfStandard::A_3u => Validator::A3_U, + }) + .unwrap_or(Validator::None), }; let buffer = typst_pdf::pdf(document, &options)?; config diff --git a/crates/typst-pdf/src/catalog_old.rs b/crates/typst-pdf/src/catalog_old.rs index 2adf26558..ca53c1732 100644 --- a/crates/typst-pdf/src/catalog_old.rs +++ b/crates/typst-pdf/src/catalog_old.rs @@ -4,11 +4,10 @@ use ecow::eco_format; use pdf_writer::types::Direction; use pdf_writer::writers::PageLabel; use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; -use typst_library::diag::{bail, SourceResult}; +use typst_library::diag::SourceResult; use typst_library::foundations::{Datetime, Smart}; use typst_library::layout::Dir; use typst_library::text::Lang; -use typst_syntax::Span; use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter}; use crate::page_old::PdfPageLabel; @@ -128,34 +127,34 @@ pub fn write_catalog( xmp.create_date(xmp_date); xmp.modify_date(xmp_date); - if ctx.options.standards.pdfa { - let mut history = xmp.history(); - history - .add_event() - .action(xmp_writer::ResourceEventAction::Saved) - .when(xmp_date) - .instance_id(&eco_format!("{instance_id}_source")); - history - .add_event() - .action(xmp_writer::ResourceEventAction::Converted) - .when(xmp_date) - .instance_id(&instance_id) - .software_agent(&creator); - } + // if ctx.options.standards.pdfa { + // let mut history = xmp.history(); + // history + // .add_event() + // .action(xmp_writer::ResourceEventAction::Saved) + // .when(xmp_date) + // .instance_id(&eco_format!("{instance_id}_source")); + // history + // .add_event() + // .action(xmp_writer::ResourceEventAction::Converted) + // .when(xmp_date) + // .instance_id(&instance_id) + // .software_agent(&creator); + // } } - // Assert dominance. - if ctx.options.standards.pdfa { - let mut extension_schemas = xmp.extension_schemas(); - extension_schemas - .xmp_media_management() - .properties() - .describe_instance_id(); - extension_schemas.pdf().properties().describe_all(); - extension_schemas.finish(); - xmp.pdfa_part(2); - xmp.pdfa_conformance("B"); - } + // // Assert dominance. + // if ctx.options.standards.pdfa { + // let mut extension_schemas = xmp.extension_schemas(); + // extension_schemas + // .xmp_media_management() + // .properties() + // .describe_instance_id(); + // extension_schemas.pdf().properties().describe_all(); + // extension_schemas.finish(); + // xmp.pdfa_part(2); + // xmp.pdfa_conformance("B"); + // } let xmp_buf = xmp.finish(None); let meta_ref = alloc.bump(); @@ -200,22 +199,22 @@ pub fn write_catalog( catalog.lang(TextStr(lang.as_str())); } - if ctx.options.standards.pdfa { - catalog - .output_intents() - .push() - .subtype(pdf_writer::types::OutputIntentSubtype::PDFA) - .output_condition(TextStr("sRGB")) - .output_condition_identifier(TextStr("Custom")) - .info(TextStr("sRGB IEC61966-2.1")) - .dest_output_profile(ctx.globals.color_functions.srgb.unwrap()); - } + // if ctx.options.standards.pdfa { + // catalog + // .output_intents() + // .push() + // .subtype(pdf_writer::types::OutputIntentSubtype::PDFA) + // .output_condition(TextStr("sRGB")) + // .output_condition_identifier(TextStr("Custom")) + // .info(TextStr("sRGB IEC61966-2.1")) + // .dest_output_profile(ctx.globals.color_functions.srgb.unwrap()); + // } catalog.finish(); - if ctx.options.standards.pdfa && pdf.refs().count() > 8388607 { - bail!(Span::detached(), "too many PDF objects"); - } + // if ctx.options.standards.pdfa && pdf.refs().count() > 8388607 { + // bail!(Span::detached(), "too many PDF objects"); + // } Ok(()) } diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs index cb2ca9492..d609d2ec9 100644 --- a/crates/typst-pdf/src/color_font.rs +++ b/crates/typst-pdf/src/color_font.rs @@ -244,9 +244,9 @@ impl ColorFontMap<()> { } let (frame, tofu) = glyph_frame(font, glyph.id); - if options.standards.pdfa && tofu { - bail!(failed_to_convert(text, glyph)); - } + // if options.standards.pdfa && tofu { + // bail!(failed_to_convert(text, glyph)); + // } let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get() * font.units_per_em(); diff --git a/crates/typst-pdf/src/color_old.rs b/crates/typst-pdf/src/color_old.rs index 4afdbc6c6..da78f5a46 100644 --- a/crates/typst-pdf/src/color_old.rs +++ b/crates/typst-pdf/src/color_old.rs @@ -149,9 +149,9 @@ pub fn alloc_color_functions_refs( let mut chunk = PdfChunk::new(); let mut used_color_spaces = ColorSpaces::default(); - if context.options.standards.pdfa { - used_color_spaces.mark_as_used(ColorSpace::Srgb); - } + // if context.options.standards.pdfa { + // used_color_spaces.mark_as_used(ColorSpace::Srgb); + // } context.resources.traverse(&mut |r| { used_color_spaces.merge(&r.colors); @@ -384,11 +384,11 @@ impl QuantizedColor for f32 { /// Fails with an error if PDF/A processing is enabled. pub(super) fn check_cmyk_allowed(options: &PdfOptions) -> SourceResult<()> { - if options.standards.pdfa { - bail!( - Span::detached(), - "cmyk colors are not currently supported by PDF/A export" - ); - } + // if options.standards.pdfa { + // bail!( + // Span::detached(), + // "cmyk colors are not currently supported by PDF/A export" + // ); + // } Ok(()) } diff --git a/crates/typst-pdf/src/content_old.rs b/crates/typst-pdf/src/content_old.rs index ff5dfc37d..e6d5b2eed 100644 --- a/crates/typst-pdf/src/content_old.rs +++ b/crates/typst-pdf/src/content_old.rs @@ -418,13 +418,13 @@ fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult /// Encode a text run into the content stream. fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> { - if ctx.options.standards.pdfa && text.font.info().is_last_resort() { - bail!( - Span::find(text.glyphs.iter().map(|g| g.span.0)), - "the text {} could not be displayed with any font", - &text.text, - ); - } + // if ctx.options.standards.pdfa && text.font.info().is_last_resort() { + // bail!( + // Span::find(text.glyphs.iter().map(|g| g.span.0)), + // "the text {} could not be displayed with any font", + // &text.text, + // ); + // } let outline_glyphs = text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count(); @@ -516,9 +516,9 @@ fn write_normal_text( // Write the glyphs with kerning adjustments. for glyph in text.glyphs() { - if ctx.options.standards.pdfa && glyph.id == 0 { - bail!(tofu(&text, glyph)); - } + // if ctx.options.standards.pdfa && glyph.id == 0 { + // bail!(tofu(&text, glyph)); + // } adjustment += glyph.x_offset; @@ -607,9 +607,9 @@ fn write_complex_glyphs( .or_default(); for glyph in text.glyphs() { - if ctx.options.standards.pdfa && glyph.id == 0 { - bail!(tofu(&text, glyph)); - } + // if ctx.options.standards.pdfa && glyph.id == 0 { + // bail!(tofu(&text, glyph)); + // } // Retrieve the Type3 font reference and the glyph index in the font. let color_fonts = ctx @@ -732,8 +732,7 @@ fn write_image( ) -> SourceResult<()> { let index = ctx.resources.images.insert(image.clone()); ctx.resources.deferred_images.entry(index).or_insert_with(|| { - let (image, color_space) = - deferred_image(image.clone(), ctx.options.standards.pdfa); + let (image, color_space) = deferred_image(image.clone(), false); if let Some(color_space) = color_space { ctx.resources.colors.mark_as_used(color_space); } @@ -749,9 +748,9 @@ fn write_image( ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); if let Some(alt) = image.alt() { - if ctx.options.standards.pdfa && alt.len() > Str::PDFA_LIMIT { - bail!(span, "the image's alt text is too long"); - } + // if ctx.options.standards.pdfa && alt.len() > Str::PDFA_LIMIT { + // bail!(span, "the image's alt text is too long"); + // } let mut image_span = ctx.content.begin_marked_content_with_properties(Name(b"Span")); diff --git a/crates/typst-pdf/src/krilla.rs b/crates/typst-pdf/src/krilla.rs index bef714817..3e8037766 100644 --- a/crates/typst-pdf/src/krilla.rs +++ b/crates/typst-pdf/src/krilla.rs @@ -1,7 +1,7 @@ use crate::primitive::{PointExt, SizeExt, TransformExt}; use crate::{paint, AbsExt, PdfOptions}; use bytemuck::TransparentWrapper; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; use krilla::action::{Action, LinkAction}; use krilla::annotation::{LinkAnnotation, Target}; use krilla::destination::XyzDestination; @@ -178,15 +178,32 @@ pub fn pdf( typst_document: &PagedDocument, options: &PdfOptions, ) -> SourceResult> { + let version = match options.pdf_version { + None => options.validator.recommended_version(), + Some(v) => { + if !options.validator.compatible_with_version(v) { + let v_string = v.as_str(); + let s_string = options.validator.as_str(); + let h_message = format!( + "export using {} instead", + options.validator.recommended_version().as_str() + ); + bail!(Span::detached(), "{v_string} is not compatible with standard {s_string}"; hint: "{h_message}"); + } else { + v + } + } + }; + let settings = SerializeSettings { compress_content_streams: true, no_device_cs: true, ascii_compatible: false, xmp_metadata: true, cmyk_profile: None, - validator: Validator::None, + validator: options.validator, enable_tagging: false, - pdf_version: PdfVersion::Pdf17, + pdf_version: version, }; let mut document = krilla::Document::new_with(settings); diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index d6e038efc..81945c994 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -18,14 +18,13 @@ mod pattern_old; mod primitive; mod resources_old; +use base64::Engine; +use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::ops::{Deref, DerefMut}; - -use base64::Engine; -use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; -use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; use typst_library::foundations::{Datetime, Smart}; use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform}; @@ -79,6 +78,9 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult { @@ -100,8 +102,10 @@ 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, - /// A list of PDF standards that Typst will enforce conformance with. - pub standards: PdfStandards, + /// The version that should be used to export the PDF. + pub pdf_version: Option, + /// A standard the PDF should conform to. + pub validator: Validator, } /// Encapsulates a list of compatible PDF standards.