This commit is contained in:
Laurenz Stampfl 2025-03-10 09:51:18 +01:00
parent 38fba0a02c
commit b464c46a8a
8 changed files with 380 additions and 227 deletions

310
Cargo.lock generated
View File

@ -217,6 +217,20 @@ name = "bytemuck"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "byteorder"
@ -749,6 +763,15 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -761,6 +784,15 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "font-types"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492"
dependencies = [
"bytemuck",
]
[[package]]
name = "fontconfig-parser"
version = "0.5.7"
@ -781,7 +813,33 @@ dependencies = [
"memmap2",
"slotmap",
"tinyvec",
"ttf-parser",
"ttf-parser 0.24.1",
]
[[package]]
name = "fontdb"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716"
dependencies = [
"fontconfig-parser",
"log",
"memmap2",
"slotmap",
"tinyvec",
"ttf-parser 0.24.1",
]
[[package]]
name = "fontdb"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
dependencies = [
"log",
"slotmap",
"tinyvec",
"ttf-parser 0.25.1",
]
[[package]]
@ -1183,6 +1241,16 @@ dependencies = [
"quick-error",
]
[[package]]
name = "image-webp"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "imagesize"
version = "0.13.0"
@ -1310,6 +1378,36 @@ dependencies = [
"libc",
]
[[package]]
name = "krilla"
version = "0.3.0"
source = "git+https://github.com/LaurenzV/krilla#478e4639001bc96118a6f651f06d42d37d0be7a8"
dependencies = [
"base64",
"bumpalo",
"flate2",
"float-cmp 0.10.0",
"fontdb 0.22.0",
"gif",
"image-webp 0.1.3",
"imagesize",
"miniz_oxide",
"once_cell",
"pdf-writer",
"resvg 0.44.0",
"rustybuzz 0.18.0",
"siphasher",
"skrifa",
"subsetter",
"tiny-skia",
"tiny-skia-path",
"usvg 0.44.0",
"xmp-writer",
"yoke",
"zune-jpeg",
"zune-png",
]
[[package]]
name = "kurbo"
version = "0.11.1"
@ -1738,9 +1836,8 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pdf-writer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5"
version = "0.12.0"
source = "git+https://github.com/LaurenzV/pdf-writer?rev=f95a19c#f95a19c07a1b3e3ee021c1199e91f19badb57d46"
dependencies = [
"bitflags 2.8.0",
"itoa",
@ -1808,7 +1905,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15afa937836bf3d876f5a04ce28810c06045857bf46c3d0d31073b8aada5494"
dependencies = [
"ttf-parser",
"ttf-parser 0.24.1",
]
[[package]]
@ -1997,6 +2094,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "read-fonts"
version = "0.22.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f"
dependencies = [
"bytemuck",
"font-types",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
@ -2048,18 +2155,35 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "resvg"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7314563c59c7ce31c18e23ad3dd092c37b928a0fa4e1c0a1a6504351ab411d1"
checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958"
dependencies = [
"gif",
"image-webp",
"image-webp 0.1.3",
"log",
"pico-args",
"rgb",
"svgtypes",
"tiny-skia",
"usvg",
"usvg 0.44.0",
"zune-jpeg",
]
[[package]]
name = "resvg"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29"
dependencies = [
"gif",
"image-webp 0.2.1",
"log",
"pico-args",
"rgb",
"svgtypes",
"tiny-skia",
"usvg 0.45.0",
"zune-jpeg",
]
@ -2130,9 +2254,27 @@ dependencies = [
"core_maths",
"log",
"smallvec",
"ttf-parser",
"unicode-bidi-mirroring",
"unicode-ccc",
"ttf-parser 0.24.1",
"unicode-bidi-mirroring 0.3.0",
"unicode-ccc 0.3.0",
"unicode-properties",
"unicode-script",
]
[[package]]
name = "rustybuzz"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [
"bitflags 2.8.0",
"bytemuck",
"core_maths",
"log",
"smallvec",
"ttf-parser 0.25.1",
"unicode-bidi-mirroring 0.4.0",
"unicode-ccc 0.4.0",
"unicode-properties",
"unicode-script",
]
@ -2315,6 +2457,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skrifa"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe"
dependencies = [
"bytemuck",
"read-fonts",
]
[[package]]
name = "slotmap"
version = "1.0.7"
@ -2361,7 +2513,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
"float-cmp",
"float-cmp 0.9.0",
]
[[package]]
@ -2405,28 +2557,7 @@ dependencies = [
[[package]]
name = "subsetter"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648"
[[package]]
name = "svg2pdf"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5014c9dadcf318fb7ef8c16438e95abcc9de1ae24d60d5bccc64c55100c50364"
dependencies = [
"fontdb",
"image",
"log",
"miniz_oxide",
"once_cell",
"pdf-writer",
"resvg",
"siphasher",
"subsetter",
"tiny-skia",
"ttf-parser",
"usvg",
]
source = "git+https://github.com/typst/subsetter?rev=172416a#172416a806246e6e9010d400d5690ca7a026e53d"
[[package]]
name = "svgtypes"
@ -2716,6 +2847,15 @@ dependencies = [
"core_maths",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [
"core_maths",
]
[[package]]
name = "two-face"
version = "0.4.3"
@ -2897,7 +3037,7 @@ dependencies = [
"ecow",
"env_proxy",
"flate2",
"fontdb",
"fontdb 0.21.0",
"native-tls",
"once_cell",
"openssl",
@ -2927,9 +3067,9 @@ dependencies = [
"icu_provider_blob",
"icu_segmenter",
"kurbo",
"rustybuzz",
"rustybuzz 0.20.1",
"smallvec",
"ttf-parser",
"ttf-parser 0.25.1",
"typst-assets",
"typst-library",
"typst-macros",
@ -2956,7 +3096,7 @@ dependencies = [
"csv",
"ecow",
"flate2",
"fontdb",
"fontdb 0.21.0",
"hayagriva",
"icu_properties",
"icu_provider",
@ -2976,7 +3116,7 @@ dependencies = [
"regex-syntax",
"roxmltree",
"rust_decimal",
"rustybuzz",
"rustybuzz 0.20.1",
"serde",
"serde_json",
"serde_yaml 0.9.34+deprecated",
@ -2985,7 +3125,7 @@ dependencies = [
"syntect",
"time",
"toml",
"ttf-parser",
"ttf-parser 0.25.1",
"two-face",
"typed-arena",
"typst-assets",
@ -2998,7 +3138,7 @@ dependencies = [
"unicode-normalization",
"unicode-segmentation",
"unscanny",
"usvg",
"usvg 0.45.0",
"wasmi",
"xmlwriter",
]
@ -3017,26 +3157,17 @@ dependencies = [
name = "typst-pdf"
version = "0.13.1"
dependencies = [
"arrayvec",
"base64",
"bytemuck",
"comemo",
"ecow",
"image",
"indexmap 2.7.1",
"miniz_oxide",
"pdf-writer",
"serde",
"subsetter",
"svg2pdf",
"ttf-parser",
"krilla",
"typst-assets",
"typst-library",
"typst-macros",
"typst-syntax",
"typst-timing",
"typst-utils",
"xmp-writer",
]
[[package]]
@ -3063,9 +3194,9 @@ dependencies = [
"comemo",
"image",
"pixglyph",
"resvg",
"resvg 0.45.0",
"tiny-skia",
"ttf-parser",
"ttf-parser 0.25.1",
"typst-library",
"typst-macros",
"typst-timing",
@ -3080,7 +3211,7 @@ dependencies = [
"ecow",
"flate2",
"image",
"ttf-parser",
"ttf-parser 0.25.1",
"typst-library",
"typst-macros",
"typst-timing",
@ -3189,12 +3320,24 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
[[package]]
name = "unicode-bidi-mirroring"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
[[package]]
name = "unicode-ccc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
[[package]]
name = "unicode-ccc"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-ident"
version = "1.0.16"
@ -3288,20 +3431,47 @@ dependencies = [
[[package]]
name = "usvg"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6803057b5cbb426e9fb8ce2216f3a9b4ca1dd2c705ba3cbebc13006e437735fd"
checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6"
dependencies = [
"base64",
"data-url",
"flate2",
"fontdb",
"fontdb 0.22.0",
"imagesize",
"kurbo",
"log",
"pico-args",
"roxmltree",
"rustybuzz",
"rustybuzz 0.18.0",
"simplecss",
"siphasher",
"strict-num",
"svgtypes",
"tiny-skia-path",
"unicode-bidi",
"unicode-script",
"unicode-vo",
"xmlwriter",
]
[[package]]
name = "usvg"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354"
dependencies = [
"base64",
"data-url",
"flate2",
"fontdb 0.23.0",
"imagesize",
"kurbo",
"log",
"pico-args",
"roxmltree",
"rustybuzz 0.20.1",
"simplecss",
"siphasher",
"strict-num",
@ -3661,8 +3831,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "xmp-writer"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487"
source = "git+https://github.com/LaurenzV/xmp-writer?rev=a1cbb887#a1cbb887a84376fea4d7590d41c194a332840549"
[[package]]
name = "xz2"
@ -3830,6 +3999,15 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.14"
@ -3838,3 +4016,13 @@ checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
dependencies = [
"zune-core",
]
[[package]]
name = "zune-png"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d29c085769c6f29effea890f093120ac019375fdc789d2a496ba8ba96c77509"
dependencies = [
"zune-core",
"zune-inflate",
]

View File

@ -70,6 +70,7 @@ if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] }
kamadak-exif = "0.6"
krilla = { git = "https://github.com/LaurenzV/krilla" }
kurbo = "0.11"
libfuzzer-sys = "0.4"
lipsum = "0.9"
@ -96,10 +97,10 @@ quote = "1"
rayon = "1.7.0"
regex = "1"
regex-syntax = "0.8"
resvg = { version = "0.43", default-features = false, features = ["raster-images"] }
resvg = { version = "0.45.0", default-features = false, features = ["raster-images"] }
roxmltree = "0.20"
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
rustybuzz = "0.18"
rustybuzz = "0.20.1"
same-file = "1"
self-replace = "1.3.7"
semver = "1"
@ -122,7 +123,7 @@ time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
tiny_http = "0.12"
tiny-skia = "0.11"
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
ttf-parser = "0.24.1"
ttf-parser = "0.25.1"
two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] }
typed-arena = "2"
unicode-bidi = "0.3.18"
@ -133,7 +134,7 @@ unicode-normalization = "0.1.24"
unicode-segmentation = "1"
unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
usvg = { version = "0.43", default-features = false, features = ["text"] }
usvg = { version = "0.45.0", default-features = false, features = ["text"] }
walkdir = "2"
wasmi = "0.40.0"
web-sys = "0.3"

View File

@ -22,7 +22,7 @@ pub struct RasterImage(Arc<Repr>);
struct Repr {
data: Bytes,
format: RasterFormat,
dynamic: image::DynamicImage,
dynamic: Arc<DynamicImage>,
icc: Option<Bytes>,
dpi: Option<f64>,
}
@ -136,7 +136,7 @@ impl RasterImage {
}
};
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
Ok(Self(Arc::new(Repr { data, format, dynamic: Arc::new(dynamic), icc, dpi })))
}
/// The raw image data.
@ -167,7 +167,7 @@ impl RasterImage {
}
/// Access the underlying dynamic image.
pub fn dynamic(&self) -> &image::DynamicImage {
pub fn dynamic(&self) -> &Arc<DynamicImage> {
&self.0.dynamic
}

View File

@ -6,9 +6,8 @@ use krilla::error::KrillaError;
use krilla::page::PageLabel;
use krilla::path::PathBuilder;
use krilla::surface::Surface;
use krilla::validation::ValidationError;
use krilla::version::PdfVersion;
use krilla::{Document, PageSettings, SerializeSettings};
use krilla::configure::{Configuration, PdfVersion, ValidationError};
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::NativeElement;
use typst_library::introspection::Location;
@ -34,7 +33,7 @@ pub fn convert(
typst_document: &PagedDocument,
options: &PdfOptions,
) -> SourceResult<Vec<u8>> {
let version = get_version(options)?;
let configuration = get_configuration(options)?;
let settings = SerializeSettings {
compress_content_streams: true,
@ -42,9 +41,8 @@ pub fn convert(
ascii_compatible: false,
xmp_metadata: true,
cmyk_profile: None,
validator: options.validator,
configuration,
enable_tagging: false,
pdf_version: version,
};
let mut document = Document::new_with(settings);
@ -295,7 +293,7 @@ pub(crate) fn handle_group(
fc.state_mut().pre_concat(group.transform);
let clip_path = group
.clip_path
.clip
.as_ref()
.and_then(|p| {
let mut builder = PathBuilder::new();
@ -321,6 +319,8 @@ pub(crate) fn handle_group(
/// Finish a krilla document and handle export errors.
fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
let validator: krilla::configure::Validator = gc.options.validator.into();
match document.finish() {
Ok(r) => Ok(r),
Err(e) => match e {
@ -336,8 +336,9 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
// We can only produce 1 error, so just take the first one.
let prefix = format!(
"validated export for {} failed:",
gc.options.validator.as_str()
validator.as_str()
);
match &ve[0] {
ValidationError::TooLongString => {
bail!(Span::detached(), "{prefix} a PDF string longer \
@ -407,6 +408,16 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
hint: "export using a different standard that supports transparency"
);
}
ValidationError::ImageInterpolation => {
bail!(Span::detached(), "{prefix} document contains an image with smooth interpolation";
hint: "such images are not supported in this export mode"
);
}
ValidationError::EmbeddedFile(_) => {
bail!(Span::detached(), "{prefix} document contains an embedded file";
hint: "embedded files are not supported in this export mode"
);
}
// The below errors cannot occur yet, only once Typst supports full PDF/A
// and PDF/UA.
@ -423,16 +434,20 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
bail!(Span::detached(), "{prefix} missing document language";
hint: "set the language of the document");
}
// Needs to be set by typst-pdf.
ValidationError::MissingHeadingTitle => {
bail!(Span::detached(), "{prefix} missing heading title";
hint: "please report this as a bug");
}
// Needs to be set by typst-pdf.
ValidationError::MissingDocumentOutline => {
bail!(Span::detached(), "{prefix} missing document outline";
hint: "please report this as a bug");
}
ValidationError::MissingTagging => {
bail!(Span::detached(), "{prefix} missing document tags";
hint: "please report this as a bug");
}
ValidationError::NoDocumentTitle => {
bail!(Span::detached(), "{prefix} missing document title";
hint: "set the title of the document");
@ -494,21 +509,31 @@ fn collect_named_destinations(
locs_to_names
}
fn get_version(options: &PdfOptions) -> SourceResult<PdfVersion> {
match options.pdf_version {
None => Ok(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();
fn get_configuration(options: &PdfOptions) -> SourceResult<Configuration> {
let config = match (options.pdf_version, options.validator) {
(None, None) => Configuration::new_with_version(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 {} instead",
options.validator.recommended_version().as_str()
v.recommended_version().as_str()
);
bail!(Span::detached(), "{v_string} is not compatible with standard {s_string}"; hint: "{h_message}");
} else {
Ok(v)
bail!(Span::detached(), "{pdf_string} is not compatible with standard {s_string}"; hint: "{h_message}");
}
}
}
};
Ok(config)
}

View File

@ -1,122 +0,0 @@
use std::collections::BTreeMap;
use ecow::EcoString;
use pdf_writer::types::AssociationKind;
use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr};
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::{NativeElement, Packed, StyleChain};
use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship};
use crate::catalog::{document_date, pdf_date};
use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs};
/// Query for all [`EmbedElem`] and write them and their file specifications.
///
/// This returns a map of embedding names and references so that we can later
/// add them to the catalog's `/Names` dictionary.
pub fn write_embedded_files(
ctx: &WithGlobalRefs,
) -> SourceResult<(PdfChunk, BTreeMap<EcoString, Ref>)> {
let mut chunk = PdfChunk::new();
let mut embedded_files = BTreeMap::default();
let elements = ctx.document.introspector.query(&EmbedElem::elem().select());
for elem in &elements {
if !ctx.options.standards.embedded_files {
// PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2,
// which we don't currently check.
bail!(
elem.span(),
"file embeddings are not currently supported for PDF/A-2";
hint: "PDF/A-3 supports arbitrary embedded files"
);
}
let embed = elem.to_packed::<EmbedElem>().unwrap();
if embed.path.derived.len() > Str::PDFA_LIMIT {
bail!(embed.span(), "embedded file path is too long");
}
let id = embed_file(ctx, &mut chunk, embed)?;
if embedded_files.insert(embed.path.derived.clone(), id).is_some() {
bail!(
elem.span(),
"duplicate embedded file for path `{}`", embed.path.derived;
hint: "embedded file paths must be unique",
);
}
}
Ok((chunk, embedded_files))
}
/// Write the embedded file stream and its file specification.
fn embed_file(
ctx: &WithGlobalRefs,
chunk: &mut PdfChunk,
embed: &Packed<EmbedElem>,
) -> SourceResult<Ref> {
let embedded_file_stream_ref = chunk.alloc.bump();
let file_spec_dict_ref = chunk.alloc.bump();
let data = embed.data.as_slice();
let compressed = deflate(data);
let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed);
embedded_file.filter(Filter::FlateDecode);
if let Some(mime_type) = embed.mime_type(StyleChain::default()) {
if mime_type.len() > Name::PDFA_LIMIT {
bail!(embed.span(), "embedded file MIME type is too long");
}
embedded_file.subtype(Name(mime_type.as_bytes()));
} else if ctx.options.standards.pdfa {
bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3");
}
let mut params = embedded_file.params();
params.size(data.len() as i32);
let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp);
if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) {
params.modification_date(pdf_date);
} else if ctx.options.standards.pdfa {
bail!(
embed.span(),
"the document must have a date when embedding files in PDF/A-3";
hint: "`set document(date: none)` must not be used in this case"
);
}
params.finish();
embedded_file.finish();
let mut file_spec = chunk.file_spec(file_spec_dict_ref);
file_spec.path(Str(embed.path.derived.as_bytes()));
file_spec.unic_file(TextStr(&embed.path.derived));
file_spec
.insert(Name(b"EF"))
.dict()
.pair(Name(b"F"), embedded_file_stream_ref)
.pair(Name(b"UF"), embedded_file_stream_ref);
if ctx.options.standards.pdfa {
// PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3.
file_spec.association_kind(match embed.relationship(StyleChain::default()) {
Some(EmbeddedFileRelationship::Source) => AssociationKind::Source,
Some(EmbeddedFileRelationship::Data) => AssociationKind::Data,
Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative,
Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement,
None => AssociationKind::Unspecified,
});
}
if let Some(description) = embed.description(StyleChain::default()) {
if description.len() > Str::PDFA_LIMIT {
bail!(embed.span(), "embedded file description is too long");
}
file_spec.description(TextStr(description));
}
Ok(file_spec_dict_ref)
}

View File

@ -1,7 +1,7 @@
use std::hash::{Hash, Hasher};
use std::sync::{Arc, OnceLock};
use image::{DynamicImage, GenericImageView, Rgba};
use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::surface::Surface;
use krilla::SvgSettings;
@ -137,7 +137,7 @@ impl CustomImage for PdfImage {
| DynamicImage::ImageRgb8(_)
| DynamicImage::ImageRgba8(_)
) {
self.raster.icc()
self.raster.icc().map(|b| b.as_bytes())
} else {
// In all other cases, the dynamic will be converted into RGB8 or LUMA8, so the ICC
// profile may become invalid, and thus we don't include it.

View File

@ -15,9 +15,6 @@ use typst_library::diag::SourceResult;
use typst_library::foundations::{Datetime, Smart};
use typst_library::layout::{PageRanges, PagedDocument};
pub use ::krilla::validation::Validator;
pub use ::krilla::version::PdfVersion;
/// Export a document into a PDF file.
///
/// Returns the raw bytes making up the PDF file.
@ -26,6 +23,72 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u
convert::convert(document, options)
}
/// The version of a PDF document.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum PdfVersion {
/// PDF 1.4.
Pdf14,
/// PDF 1.5.
Pdf15,
/// PDF 1.6.
Pdf16,
/// PDF 1.7.
Pdf17,
/// PDF 2.0.
Pdf20,
}
impl From<PdfVersion> 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)]
pub enum Validator {
/// The validator for the PDF/A1-A standard.
A1_A,
/// The validator for the PDF/A1-B standard.
A1_B,
/// The validator for the PDF/A2-B standard.
A2_B,
/// The validator for the PDF/A2-U standard.
A2_U,
/// The validator for the PDF/A3-B standard.
A3_B,
/// The validator for the PDF/A3-U standard.
A3_U,
/// The validator for the PDF/A4 standard.
A4,
/// The validator for the PDF/A4f standard.
A4F,
/// The validator for the PDF/A4e standard.
A4E,
}
impl From<Validator> for krilla::configure::Validator {
fn from(value: Validator) -> Self {
match value {
Validator::A1_A => krilla::configure::Validator::A1_A,
Validator::A1_B => krilla::configure::Validator::A1_B,
Validator::A2_B => krilla::configure::Validator::A2_B,
Validator::A2_U => krilla::configure::Validator::A2_U,
Validator::A3_B => krilla::configure::Validator::A3_B,
Validator::A3_U => krilla::configure::Validator::A3_U,
Validator::A4 => krilla::configure::Validator::A4,
Validator::A4F => krilla::configure::Validator::A4F,
Validator::A4E => krilla::configure::Validator::A4E,
}
}
}
/// Settings for PDF export.
#[derive(Debug, Default)]
pub struct PdfOptions<'a> {
@ -50,7 +113,7 @@ pub struct PdfOptions<'a> {
/// The version that should be used to export the PDF.
pub pdf_version: Option<PdfVersion>,
/// A standard the PDF should conform to.
pub validator: Validator,
pub validator: Option<Validator>,
}
/// A timestamp with timezone information.

View File

@ -6,9 +6,7 @@ use krilla::path as kp;
use krilla::path::PathBuilder;
use typst_library::layout::{Abs, Point, Size, Transform};
use typst_library::text::Font;
use typst_library::visualize::{
Color, ColorSpace, FillRule, LineCap, LineJoin, Path, PathItem,
};
use typst_library::visualize::{Color, ColorSpace, Curve, CurveItem, FillRule, LineCap, LineJoin};
pub(crate) trait SizeExt {
fn to_krilla(&self) -> kg::Size;
@ -118,12 +116,12 @@ pub(crate) fn display_font(font: &Font) -> String {
}
/// Build a typst path using a path builder.
pub(crate) fn convert_path(path: &Path, builder: &mut PathBuilder) {
pub(crate) fn convert_path(path: &Curve, builder: &mut PathBuilder) {
for item in &path.0 {
match item {
PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),
PathItem::LineTo(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()),
PathItem::CubicTo(p1, p2, p3) => builder.cubic_to(
CurveItem::Move(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),
CurveItem::Line(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()),
CurveItem::Cubic(p1, p2, p3) => builder.cubic_to(
p1.x.to_f32(),
p1.y.to_f32(),
p2.x.to_f32(),
@ -131,7 +129,7 @@ pub(crate) fn convert_path(path: &Path, builder: &mut PathBuilder) {
p3.x.to_f32(),
p3.y.to_f32(),
),
PathItem::ClosePath => builder.close(),
CurveItem::Close => builder.close(),
}
}
}