Add support for exporting to specific version

This commit is contained in:
Laurenz Stampfl 2024-12-15 22:22:46 +01:00
parent 9a4bd9be25
commit 880aa4b8a1
8 changed files with 162 additions and 105 deletions

View File

@ -241,10 +241,14 @@ pub struct CompileArgs {
#[arg(long = "pages", value_delimiter = ',')] #[arg(long = "pages", value_delimiter = ',')]
pub pages: Option<Vec<Pages>>, pub pages: Option<Vec<Pages>>,
/// 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<PdfVersion>,
/// A PDF standard that Typst will enforce
/// conformance with. /// conformance with.
#[arg(long = "pdf-standard", value_delimiter = ',')] #[arg(long = "pdf-standard")]
pub pdf_standard: Vec<PdfStandard>, pub pdf_standard: Option<PdfStandard>,
/// The PPI (pixels per inch) to use for PNG export. /// The PPI (pixels per inch) to use for PNG export.
#[arg(long = "ppi", default_value_t = 144.0)] #[arg(long = "ppi", default_value_t = 144.0)]
@ -463,16 +467,45 @@ pub enum Feature {
display_possible_values!(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. /// A PDF standard that Typst can enforce conformance with.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum PdfStandard { pub enum PdfStandard {
/// PDF 1.7. /// PDF/A-2u.
#[value(name = "1.7")] #[value(name = "a-1b")]
V_1_7, A_1b,
/// PDF/A-2b. /// PDF/A-2b.
#[value(name = "a-2b")] #[value(name = "a-2b")]
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); display_possible_values!(PdfStandard);

View File

@ -17,11 +17,11 @@ use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span}; use typst::syntax::{FileId, Source, Span};
use typst::WorldExt; use typst::WorldExt;
use typst_pdf::{PdfOptions, PdfStandards}; use typst_pdf::{PdfOptions, PdfStandards, Validator};
use crate::args::{ use crate::args::{
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
PdfStandard, WatchCommand, PdfStandard, PdfVersion, WatchCommand,
}; };
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
use crate::server::HtmlServer; use crate::server::HtmlServer;
@ -62,9 +62,10 @@ pub struct CompileConfig {
/// Opens the output file with the default viewer or a specific program after /// Opens the output file with the default viewer or a specific program after
/// compilation. /// compilation.
pub open: Option<Option<String>>, pub open: Option<Option<String>>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce /// The version that should be used to export the PDF.
/// conformance with. pub pdf_version: Option<PdfVersion>,
pub pdf_standards: PdfStandards, /// A standard the PDF should conform to.
pub pdf_standard: Option<PdfStandard>,
/// A path to write a Makefile rule describing the current compilation. /// A path to write a Makefile rule describing the current compilation.
pub make_deps: Option<PathBuf>, pub make_deps: Option<PathBuf>,
/// The PPI (pixels per inch) to use for PNG export. /// 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()) 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::<Vec<_>>();
PdfStandards::new(&list)?
};
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
let server = match watch { let server = match watch {
Some(command) Some(command)
@ -157,15 +146,16 @@ impl CompileConfig {
output, output,
output_format, output_format,
pages, pages,
pdf_standards,
creation_timestamp: args.world.creation_timestamp, creation_timestamp: args.world.creation_timestamp,
make_deps: args.make_deps.clone(), make_deps: args.make_deps.clone(),
ppi: args.ppi, ppi: args.ppi,
diagnostic_format: args.process.diagnostic_format, diagnostic_format: args.process.diagnostic_format,
open: args.open.clone(), open: args.open.clone(),
pdf_version: args.pdf_version,
export_cache: ExportCache::new(), export_cache: ExportCache::new(),
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
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), config.creation_timestamp.unwrap_or_else(chrono::Utc::now),
), ),
page_ranges: config.pages.clone(), 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)?; let buffer = typst_pdf::pdf(document, &options)?;
config config

View File

@ -4,11 +4,10 @@ use ecow::eco_format;
use pdf_writer::types::Direction; use pdf_writer::types::Direction;
use pdf_writer::writers::PageLabel; use pdf_writer::writers::PageLabel;
use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; 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::foundations::{Datetime, Smart};
use typst_library::layout::Dir; use typst_library::layout::Dir;
use typst_library::text::Lang; use typst_library::text::Lang;
use typst_syntax::Span;
use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter}; use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter};
use crate::page_old::PdfPageLabel; use crate::page_old::PdfPageLabel;
@ -128,34 +127,34 @@ pub fn write_catalog(
xmp.create_date(xmp_date); xmp.create_date(xmp_date);
xmp.modify_date(xmp_date); xmp.modify_date(xmp_date);
if ctx.options.standards.pdfa { // if ctx.options.standards.pdfa {
let mut history = xmp.history(); // let mut history = xmp.history();
history // history
.add_event() // .add_event()
.action(xmp_writer::ResourceEventAction::Saved) // .action(xmp_writer::ResourceEventAction::Saved)
.when(xmp_date) // .when(xmp_date)
.instance_id(&eco_format!("{instance_id}_source")); // .instance_id(&eco_format!("{instance_id}_source"));
history // history
.add_event() // .add_event()
.action(xmp_writer::ResourceEventAction::Converted) // .action(xmp_writer::ResourceEventAction::Converted)
.when(xmp_date) // .when(xmp_date)
.instance_id(&instance_id) // .instance_id(&instance_id)
.software_agent(&creator); // .software_agent(&creator);
} // }
} }
// Assert dominance. // // Assert dominance.
if ctx.options.standards.pdfa { // if ctx.options.standards.pdfa {
let mut extension_schemas = xmp.extension_schemas(); // let mut extension_schemas = xmp.extension_schemas();
extension_schemas // extension_schemas
.xmp_media_management() // .xmp_media_management()
.properties() // .properties()
.describe_instance_id(); // .describe_instance_id();
extension_schemas.pdf().properties().describe_all(); // extension_schemas.pdf().properties().describe_all();
extension_schemas.finish(); // extension_schemas.finish();
xmp.pdfa_part(2); // xmp.pdfa_part(2);
xmp.pdfa_conformance("B"); // xmp.pdfa_conformance("B");
} // }
let xmp_buf = xmp.finish(None); let xmp_buf = xmp.finish(None);
let meta_ref = alloc.bump(); let meta_ref = alloc.bump();
@ -200,22 +199,22 @@ pub fn write_catalog(
catalog.lang(TextStr(lang.as_str())); catalog.lang(TextStr(lang.as_str()));
} }
if ctx.options.standards.pdfa { // if ctx.options.standards.pdfa {
catalog // catalog
.output_intents() // .output_intents()
.push() // .push()
.subtype(pdf_writer::types::OutputIntentSubtype::PDFA) // .subtype(pdf_writer::types::OutputIntentSubtype::PDFA)
.output_condition(TextStr("sRGB")) // .output_condition(TextStr("sRGB"))
.output_condition_identifier(TextStr("Custom")) // .output_condition_identifier(TextStr("Custom"))
.info(TextStr("sRGB IEC61966-2.1")) // .info(TextStr("sRGB IEC61966-2.1"))
.dest_output_profile(ctx.globals.color_functions.srgb.unwrap()); // .dest_output_profile(ctx.globals.color_functions.srgb.unwrap());
} // }
catalog.finish(); catalog.finish();
if ctx.options.standards.pdfa && pdf.refs().count() > 8388607 { // if ctx.options.standards.pdfa && pdf.refs().count() > 8388607 {
bail!(Span::detached(), "too many PDF objects"); // bail!(Span::detached(), "too many PDF objects");
} // }
Ok(()) Ok(())
} }

View File

@ -244,9 +244,9 @@ impl ColorFontMap<()> {
} }
let (frame, tofu) = glyph_frame(font, glyph.id); let (frame, tofu) = glyph_frame(font, glyph.id);
if options.standards.pdfa && tofu { // if options.standards.pdfa && tofu {
bail!(failed_to_convert(text, glyph)); // bail!(failed_to_convert(text, glyph));
} // }
let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get() let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get()
* font.units_per_em(); * font.units_per_em();

View File

@ -149,9 +149,9 @@ pub fn alloc_color_functions_refs(
let mut chunk = PdfChunk::new(); let mut chunk = PdfChunk::new();
let mut used_color_spaces = ColorSpaces::default(); let mut used_color_spaces = ColorSpaces::default();
if context.options.standards.pdfa { // if context.options.standards.pdfa {
used_color_spaces.mark_as_used(ColorSpace::Srgb); // used_color_spaces.mark_as_used(ColorSpace::Srgb);
} // }
context.resources.traverse(&mut |r| { context.resources.traverse(&mut |r| {
used_color_spaces.merge(&r.colors); used_color_spaces.merge(&r.colors);
@ -384,11 +384,11 @@ impl QuantizedColor for f32 {
/// Fails with an error if PDF/A processing is enabled. /// Fails with an error if PDF/A processing is enabled.
pub(super) fn check_cmyk_allowed(options: &PdfOptions) -> SourceResult<()> { pub(super) fn check_cmyk_allowed(options: &PdfOptions) -> SourceResult<()> {
if options.standards.pdfa { // if options.standards.pdfa {
bail!( // bail!(
Span::detached(), // Span::detached(),
"cmyk colors are not currently supported by PDF/A export" // "cmyk colors are not currently supported by PDF/A export"
); // );
} // }
Ok(()) Ok(())
} }

View File

@ -418,13 +418,13 @@ fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult
/// Encode a text run into the content stream. /// Encode a text run into the content stream.
fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> { fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> {
if ctx.options.standards.pdfa && text.font.info().is_last_resort() { // if ctx.options.standards.pdfa && text.font.info().is_last_resort() {
bail!( // bail!(
Span::find(text.glyphs.iter().map(|g| g.span.0)), // Span::find(text.glyphs.iter().map(|g| g.span.0)),
"the text {} could not be displayed with any font", // "the text {} could not be displayed with any font",
&text.text, // &text.text,
); // );
} // }
let outline_glyphs = let outline_glyphs =
text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count(); 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. // Write the glyphs with kerning adjustments.
for glyph in text.glyphs() { for glyph in text.glyphs() {
if ctx.options.standards.pdfa && glyph.id == 0 { // if ctx.options.standards.pdfa && glyph.id == 0 {
bail!(tofu(&text, glyph)); // bail!(tofu(&text, glyph));
} // }
adjustment += glyph.x_offset; adjustment += glyph.x_offset;
@ -607,9 +607,9 @@ fn write_complex_glyphs(
.or_default(); .or_default();
for glyph in text.glyphs() { for glyph in text.glyphs() {
if ctx.options.standards.pdfa && glyph.id == 0 { // if ctx.options.standards.pdfa && glyph.id == 0 {
bail!(tofu(&text, glyph)); // bail!(tofu(&text, glyph));
} // }
// Retrieve the Type3 font reference and the glyph index in the font. // Retrieve the Type3 font reference and the glyph index in the font.
let color_fonts = ctx let color_fonts = ctx
@ -732,8 +732,7 @@ fn write_image(
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = ctx.resources.images.insert(image.clone()); let index = ctx.resources.images.insert(image.clone());
ctx.resources.deferred_images.entry(index).or_insert_with(|| { ctx.resources.deferred_images.entry(index).or_insert_with(|| {
let (image, color_space) = let (image, color_space) = deferred_image(image.clone(), false);
deferred_image(image.clone(), ctx.options.standards.pdfa);
if let Some(color_space) = color_space { if let Some(color_space) = color_space {
ctx.resources.colors.mark_as_used(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]); ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]);
if let Some(alt) = image.alt() { if let Some(alt) = image.alt() {
if ctx.options.standards.pdfa && alt.len() > Str::PDFA_LIMIT { // if ctx.options.standards.pdfa && alt.len() > Str::PDFA_LIMIT {
bail!(span, "the image's alt text is too long"); // bail!(span, "the image's alt text is too long");
} // }
let mut image_span = let mut image_span =
ctx.content.begin_marked_content_with_properties(Name(b"Span")); ctx.content.begin_marked_content_with_properties(Name(b"Span"));

View File

@ -1,7 +1,7 @@
use crate::primitive::{PointExt, SizeExt, TransformExt}; use crate::primitive::{PointExt, SizeExt, TransformExt};
use crate::{paint, AbsExt, PdfOptions}; use crate::{paint, AbsExt, PdfOptions};
use bytemuck::TransparentWrapper; use bytemuck::TransparentWrapper;
use ecow::EcoString; use ecow::{eco_format, EcoString};
use krilla::action::{Action, LinkAction}; use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target}; use krilla::annotation::{LinkAnnotation, Target};
use krilla::destination::XyzDestination; use krilla::destination::XyzDestination;
@ -178,15 +178,32 @@ pub fn pdf(
typst_document: &PagedDocument, typst_document: &PagedDocument,
options: &PdfOptions, options: &PdfOptions,
) -> SourceResult<Vec<u8>> { ) -> SourceResult<Vec<u8>> {
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 { let settings = SerializeSettings {
compress_content_streams: true, compress_content_streams: true,
no_device_cs: true, no_device_cs: true,
ascii_compatible: false, ascii_compatible: false,
xmp_metadata: true, xmp_metadata: true,
cmyk_profile: None, cmyk_profile: None,
validator: Validator::None, validator: options.validator,
enable_tagging: false, enable_tagging: false,
pdf_version: PdfVersion::Pdf17, pdf_version: version,
}; };
let mut document = krilla::Document::new_with(settings); let mut document = krilla::Document::new_with(settings);

View File

@ -18,14 +18,13 @@ mod pattern_old;
mod primitive; mod primitive;
mod resources_old; 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::collections::HashMap;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::ops::{Deref, DerefMut}; 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::diag::{bail, SourceResult, StrResult};
use typst_library::foundations::{Datetime, Smart}; use typst_library::foundations::{Datetime, Smart};
use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform}; use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform};
@ -79,6 +78,9 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u
// .export_with(write_catalog) // .export_with(write_catalog)
} }
pub use ::krilla::validation::Validator;
pub use ::krilla::version::PdfVersion;
/// Settings for PDF export. /// Settings for PDF export.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct PdfOptions<'a> { pub struct PdfOptions<'a> {
@ -100,8 +102,10 @@ pub struct PdfOptions<'a> {
/// Specifies which ranges of pages should be exported in the PDF. When /// Specifies which ranges of pages should be exported in the PDF. When
/// `None`, all pages should be exported. /// `None`, all pages should be exported.
pub page_ranges: Option<PageRanges>, pub page_ranges: Option<PageRanges>,
/// A list of PDF standards that Typst will enforce conformance with. /// The version that should be used to export the PDF.
pub standards: PdfStandards, pub pdf_version: Option<PdfVersion>,
/// A standard the PDF should conform to.
pub validator: Validator,
} }
/// Encapsulates a list of compatible PDF standards. /// Encapsulates a list of compatible PDF standards.