From 4b733b5f8b8c6c978fb91b9ae469ec4794cced49 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 17 Jul 2025 19:28:41 +0200 Subject: [PATCH 01/19] First version --- .github/workflows/ci.yml | 4 +- Cargo.lock | 117 ++++++++--- Cargo.toml | 6 +- crates/typst-cli/src/args.rs | 1 + crates/typst-cli/src/world.rs | 1 + crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/lib.rs | 1 + .../typst-library/src/visualize/image/mod.rs | 60 +++++- .../typst-library/src/visualize/image/pdf.rs | 184 ++++++++++++++++++ crates/typst-pdf/src/convert.rs | 24 ++- crates/typst-pdf/src/image.rs | 21 +- crates/typst-pdf/src/link.rs | 3 - crates/typst-render/Cargo.toml | 1 + crates/typst-render/src/image.rs | 70 ++++++- crates/typst-svg/src/image.rs | 1 + 15 files changed, 448 insertions(+), 47 deletions(-) create mode 100644 crates/typst-library/src/visualize/image/pdf.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70518860e..369a2d5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: sudo dpkg --add-architecture i386 sudo apt update sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.88.0 with: targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }} - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.88.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 diff --git a/Cargo.lock b/Cargo.lock index dcd154367..d08dc9775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -214,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" dependencies = [ "bytemuck_derive", ] @@ -964,6 +964,69 @@ dependencies = [ "url", ] +[[package]] +name = "hayro" +version = "0.1.0" +source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "hayro-font" +version = "0.1.0" +source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +dependencies = [ + "log", + "phf", +] + +[[package]] +name = "hayro-interpret" +version = "0.1.0" +source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +dependencies = [ + "bitflags 2.9.1", + "hayro-font", + "hayro-syntax", + "kurbo", + "log", + "phf", + "qcms", + "skrifa", + "smallvec", + "yoke 0.8.0", +] + +[[package]] +name = "hayro-syntax" +version = "0.0.1" +source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +dependencies = [ + "flate2", + "kurbo", + "log", + "rustc-hash", + "smallvec", + "zune-jpeg", +] + +[[package]] +name = "hayro-write" +version = "0.1.0" +source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +dependencies = [ + "flate2", + "hayro-syntax", + "log", + "pdf-writer", +] + [[package]] name = "heck" version = "0.5.0" @@ -1206,9 +1269,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "image" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", @@ -1271,7 +1334,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -1367,7 +1430,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.4.0" -source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" +source = "git+https://github.com/LaurenzV/krilla?rev=f7d753c3#f7d753c39e1fb7118fb1b774243a0720771f229f" dependencies = [ "base64", "bumpalo", @@ -1376,6 +1439,7 @@ dependencies = [ "float-cmp 0.10.0", "fxhash", "gif", + "hayro-write", "image-webp", "imagesize", "once_cell", @@ -1385,6 +1449,7 @@ dependencies = [ "rustybuzz", "siphasher", "skrifa", + "smallvec", "subsetter", "tiny-skia-path", "xmp-writer", @@ -1395,7 +1460,7 @@ dependencies = [ [[package]] name = "krilla-svg" version = "0.1.0" -source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" +source = "git+https://github.com/LaurenzV/krilla?rev=f7d753c3#f7d753c39e1fb7118fb1b774243a0720771f229f" dependencies = [ "flate2", "fontdb", @@ -1408,9 +1473,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" dependencies = [ "arrayvec", "smallvec", @@ -1462,7 +1527,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -1628,7 +1693,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "filetime", "fsevent-sys", "inotify", @@ -1710,7 +1775,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -1847,7 +1912,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "itoa", "memchr", "ryu", @@ -2005,7 +2070,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "getopts", "memchr", "unicase", @@ -2118,7 +2183,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] @@ -2221,7 +2286,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -2240,7 +2305,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "bytemuck", "core_maths", "log", @@ -2288,7 +2353,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -2451,9 +2516,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "spin" @@ -3063,7 +3128,7 @@ name = "typst-library" version = "0.13.1" dependencies = [ "az", - "bitflags 2.8.0", + "bitflags 2.9.1", "bumpalo", "chinese-number", "ciborium", @@ -3075,6 +3140,7 @@ dependencies = [ "fontdb", "glidesort", "hayagriva", + "hayro-syntax", "icu_properties", "icu_provider", "icu_provider_blob", @@ -3173,6 +3239,7 @@ version = "0.13.1" dependencies = [ "bytemuck", "comemo", + "hayro", "image", "pixglyph", "resvg", @@ -3589,7 +3656,7 @@ version = "0.221.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "indexmap 2.7.1", ] @@ -3724,7 +3791,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 63ea32b94..07ba7fb0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,8 @@ fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" glidesort = "0.1.2" hayagriva = "0.8.1" +hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "d385360" } +hayro = { git = "https://github.com/LaurenzV/hayro", rev = "d385360" } heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -73,8 +75,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" -krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] } -krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" } +krilla = { git = "https://github.com/LaurenzV/krilla", rev = "f7d753c3", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "f7d753c3" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 7459be0f2..093735cbb 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -471,6 +471,7 @@ display_possible_values!(DiagnosticFormat); #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] pub enum Feature { Html, + PdfEmbedding, } display_possible_values!(Feature); diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 8ad766b14..ab5216831 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -117,6 +117,7 @@ impl SystemWorld { .iter() .map(|&feature| match feature { Feature::Html => typst::Feature::Html, + Feature::PdfEmbedding => typst::Feature::PdfEmbedding, }) .collect(); diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index f4b219882..2297f7ca7 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -31,6 +31,7 @@ flate2 = { workspace = true } fontdb = { workspace = true } glidesort = { workspace = true } hayagriva = { workspace = true } +hayro-syntax = { workspace = true } icu_properties = { workspace = true } icu_provider = { workspace = true } icu_provider_blob = { workspace = true } diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 025e997c6..edbc182fa 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -237,6 +237,7 @@ impl FromIterator for Features { #[non_exhaustive] pub enum Feature { Html, + PdfEmbedding, } /// A group of related standard library definitions. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index f1fa6381b..e9b6df993 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -1,8 +1,10 @@ //! Image handling. +mod pdf; mod raster; mod svg; +pub use self::pdf::PdfImage; pub use self::raster::{ ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage, }; @@ -12,11 +14,12 @@ use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; +use typst_library::World; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; +use crate::diag::{bail, error, warning, At, LoadedWithin, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, @@ -26,6 +29,7 @@ use crate::layout::{Length, Rel, Sizing}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::model::Figurable; use crate::text::{families, LocalName}; +use crate::visualize::image::pdf::PdfDocument; /// A raster or vector graphic. /// @@ -126,6 +130,11 @@ pub struct ImageElem { /// A text describing the image. pub alt: Option, + /// The page number that should be embedded as an image. This attribute only has an effect + /// for PDF files. + #[default(1)] + pub page: usize, + /// How the image should adjust itself to a given area (the area is defined /// by the `width` and `height` fields). Note that `fit` doesn't visually /// change anything if the area's aspect ratio is the same as the image's @@ -261,6 +270,34 @@ impl Packed { ) .within(loaded)?, ), + ImageFormat::Vector(VectorFormat::Pdf) => { + let document = + PdfDocument::new(loaded.data.clone(), engine.world.clone()) + .within(loaded)?; + let page_num = self.page.get(styles); + + if page_num == 0 { + bail!( + span, + "{page_num} is not a valid page number"; + hint: "page numbers start at 1" + ) + }; + + let page_idx = page_num - 1; + let num_pages = document.len(); + // The user provides the page number staring from 1, further down the pipeline they page + // numbers are 0-based. + let Some(pdf_image) = PdfImage::new(document, page_idx) else { + bail!( + span, + "page {page_num} doesn't exist"; + hint: "the document only has {num_pages} pages" + ) + }; + + ImageKind::Pdf(pdf_image) + } }; Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles))) @@ -286,6 +323,7 @@ impl Packed { "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), "gif" => return Ok(ExchangeFormat::Gif.into()), "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), + "pdf" => return Ok(VectorFormat::Pdf.into()), "webp" => return Ok(ExchangeFormat::Webp.into()), _ => {} } @@ -373,6 +411,7 @@ impl Image { match &self.0.kind { ImageKind::Raster(raster) => raster.format().into(), ImageKind::Svg(_) => VectorFormat::Svg.into(), + ImageKind::Pdf(_) => VectorFormat::Pdf.into(), } } @@ -381,6 +420,7 @@ impl Image { match &self.0.kind { ImageKind::Raster(raster) => raster.width() as f64, ImageKind::Svg(svg) => svg.width(), + ImageKind::Pdf(pdf) => pdf.width() as f64, } } @@ -389,6 +429,7 @@ impl Image { match &self.0.kind { ImageKind::Raster(raster) => raster.height() as f64, ImageKind::Svg(svg) => svg.height(), + ImageKind::Pdf(pdf) => pdf.height() as f64, } } @@ -397,6 +438,7 @@ impl Image { match &self.0.kind { ImageKind::Raster(raster) => raster.dpi(), ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI), + ImageKind::Pdf(_) => Some(Image::DEFAULT_DPI), } } @@ -435,6 +477,8 @@ pub enum ImageKind { Raster(RasterImage), /// An SVG image. Svg(SvgImage), + /// A PDF image. + Pdf(PdfImage), } impl From for ImageKind { @@ -469,10 +513,20 @@ impl ImageFormat { return Some(Self::Vector(VectorFormat::Svg)); } + if is_pdf(data) { + return Some(Self::Vector(VectorFormat::Pdf)); + } + None } } +/// Checks whether the data looks like a PDF file. +fn is_pdf(data: &[u8]) -> bool { + let head = &data[..data.len().min(2048)]; + memchr::memmem::find(head, b"%PDF-").is_some() +} + /// Checks whether the data looks like an SVG or a compressed SVG. fn is_svg(data: &[u8]) -> bool { // Check for the gzip magic bytes. This check is perhaps a bit too @@ -493,6 +547,8 @@ fn is_svg(data: &[u8]) -> bool { pub enum VectorFormat { /// The vector graphics format of the web. Svg, + /// The PDF graphics format. + Pdf, } impl From for ImageFormat diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs new file mode 100644 index 000000000..7f541e1e6 --- /dev/null +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -0,0 +1,184 @@ +use crate::diag::{LoadError, LoadResult, SourceResult}; +use crate::foundations::Bytes; +use crate::text::{FontStretch, FontStyle, FontVariant, FontWeight}; +use crate::World; +use comemo::Tracked; +use hayro_syntax::page::Page; +use hayro_syntax::Pdf; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +struct DocumentRepr { + pdf: Arc, + data: Bytes, + standard_fonts: Arc, +} + +impl Hash for DocumentRepr { + fn hash(&self, state: &mut H) { + self.data.hash(state); + } +} + +/// A PDF document. +#[derive(Clone, Hash)] +pub struct PdfDocument(Arc); + +impl PdfDocument { + /// Load a PDF document. + #[comemo::memoize] + #[typst_macros::time(name = "load pdf document")] + pub fn new(data: Bytes, world: Tracked) -> LoadResult { + // TODO: Remove unwraps + let pdf = Arc::new(Pdf::new(Arc::new(data.clone())).unwrap()); + let standard_fonts = get_standard_fonts(world.clone()); + + Ok(Self(Arc::new(DocumentRepr { data, pdf, standard_fonts }))) + } + + pub fn len(&self) -> usize { + self.0.pdf.pages().len() + } +} + +struct ImageRepr { + pub document: PdfDocument, + pub page_index: usize, + pub width: f32, + pub height: f32, +} + +impl Hash for ImageRepr { + fn hash(&self, state: &mut H) { + self.document.hash(state); + self.page_index.hash(state); + } +} + +/// A page of a PDF file. +#[derive(Clone, Hash)] +pub struct PdfImage(Arc); + +impl PdfImage { + /// Create a new PDF image. Returns `None` if the page index is not valid. + #[comemo::memoize] + pub fn new(document: PdfDocument, page: usize) -> Option { + // TODO: Don't allow loading if pdf-embedding feature is disabled. + // TODO: Remove Unwrap + let dimensions = document.0.pdf.pages().get(page)?.render_dimensions(); + + Some(Self(Arc::new(ImageRepr { + document, + page_index: page, + width: dimensions.0, + height: dimensions.1, + }))) + } + + pub fn page(&self) -> &Page { + &self.0.document.0.pdf.pages()[self.0.page_index] + } + + pub fn pdf(&self) -> &Arc { + &self.0.document.0.pdf + } + + pub fn width(&self) -> f32 { + self.0.width + } + + pub fn standard_fonts(&self) -> &Arc { + &self.0.document.0.standard_fonts + } + + pub fn height(&self) -> f32 { + self.0.height + } + + pub fn data(&self) -> &Bytes { + &self.0.document.0.data + } + + pub fn page_index(&self) -> usize { + self.0.page_index + } + + pub fn document(&self) -> &PdfDocument { + &self.0.document + } +} + +#[comemo::memoize] +fn get_standard_fonts(world: Tracked) -> Arc { + let book = world.book(); + + let get_font = |name: &str, fallback_name: Option<&str>, variant: FontVariant| { + book.select(name, variant) + .or_else(|| { + if let Some(fallback_name) = fallback_name { + book.select(fallback_name, variant) + } else { + None + } + }) + .and_then(|i| world.font(i)) + .map(|font| font.data().clone()) + }; + + let normal_variant = FontVariant::new( + FontStyle::Normal, + FontWeight::default(), + FontStretch::default(), + ); + let bold_variant = + FontVariant::new(FontStyle::Normal, FontWeight::BOLD, FontStretch::default()); + let italic_variant = FontVariant::new( + FontStyle::Italic, + FontWeight::default(), + FontStretch::default(), + ); + let bold_italic_variant = + FontVariant::new(FontStyle::Italic, FontWeight::BOLD, FontStretch::default()); + + let helvetica = VariantFont { + normal: get_font("helvetica", Some("liberation sans"), normal_variant), + bold: get_font("helvetica", Some("liberation sans"), bold_variant), + italic: get_font("helvetica", Some("liberation sans"), italic_variant), + bold_italic: get_font("helvetica", Some("liberation sans"), bold_italic_variant), + }; + + let courier = VariantFont { + normal: get_font("courier", Some("liberation mono"), normal_variant), + bold: get_font("courier", Some("liberation mono"), bold_variant), + italic: get_font("courier", Some("liberation mono"), italic_variant), + bold_italic: get_font("courier", Some("liberation mono"), bold_italic_variant), + }; + + let times = VariantFont { + normal: get_font("times", Some("liberation serif"), normal_variant), + bold: get_font("times", Some("liberation serif"), bold_variant), + italic: get_font("times", Some("liberation serif"), italic_variant), + bold_italic: get_font("times", Some("liberation serif"), bold_italic_variant), + }; + + // TODO: Use Foxit fonts as fallback + let symbol = get_font("symbol", None, normal_variant); + let zapf_dingbats = get_font("zapf dingbats", None, normal_variant); + + Arc::new(StandardFonts { helvetica, courier, times, symbol, zapf_dingbats }) +} + +pub struct VariantFont { + pub normal: Option, + pub bold: Option, + pub italic: Option, + pub bold_italic: Option, +} + +pub struct StandardFonts { + pub helvetica: VariantFont, + pub courier: VariantFont, + pub times: VariantFont, + pub symbol: Option, + pub zapf_dingbats: Option, +} diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 9e2aa87b7..0849545e7 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -363,6 +363,25 @@ fn finish( hint: "convert the image to 8 bit instead" ) } + KrillaError::Pdf(_, e, loc) => { + // TODO: Better errors + let span = to_span(loc); + bail!(span, "failed to process PDF"); + } + KrillaError::DuplicateTagId(_, loc) => { + let span = to_span(loc); + bail!(span, + "duplicate tag id"; + hint: "this is a bug in typst, please report it" + ); + } + KrillaError::UnknownTagId(_, loc) => { + let span = to_span(loc); + bail!(span, + "unknown tag id"; + hint: "this is a bug in typst, please report it" + ); + } }, } } @@ -535,12 +554,12 @@ fn convert_error( } // The below errors cannot occur yet, only once Typst supports full PDF/A // and PDF/UA. But let's still add a message just to be on the safe side. - ValidationError::MissingAnnotationAltText => error!( + ValidationError::MissingAnnotationAltText(_) => error!( Span::detached(), "{prefix} missing annotation alt text"; hint: "please report this as a bug" ), - ValidationError::MissingAltText => error!( + ValidationError::MissingAltText(_) => error!( Span::detached(), "{prefix} missing alt text"; hint: "make sure your images and equations have alt text" @@ -576,6 +595,7 @@ fn convert_error( "{prefix} missing document date"; hint: "set the date of the document" ), + ValidationError::EmbeddedPDF(loc) => error!(to_span(*loc), "TODO"), } } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 93bdb1950..e23652a05 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -3,13 +3,14 @@ use std::sync::{Arc, OnceLock}; use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba}; use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; +use krilla::pdf::PdfDocument; use krilla::surface::Surface; use krilla_svg::{SurfaceExt, SvgSettings}; use typst_library::diag::{bail, SourceResult}; use typst_library::foundations::Smart; use typst_library::layout::{Abs, Angle, Ratio, Size, Transform}; use typst_library::visualize::{ - ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, RasterImage, + ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage, }; use typst_syntax::Span; @@ -60,6 +61,9 @@ pub(crate) fn handle_image( SvgSettings { embed_text: true, ..Default::default() }, ); } + ImageKind::Pdf(pdf) => { + surface.draw_pdf_page(&convert_pdf(pdf), size.to_krilla(), pdf.page_index()) + } } if image.alt().is_some() { @@ -85,9 +89,9 @@ struct Repr { /// A wrapper around `RasterImage` so that we can implement `CustomImage`. #[derive(Clone)] -struct PdfImage(Arc); +struct PdfRasterImage(Arc); -impl PdfImage { +impl PdfRasterImage { pub fn new(raster: RasterImage) -> Self { Self(Arc::new(Repr { raster, @@ -97,7 +101,7 @@ impl PdfImage { } } -impl Hash for PdfImage { +impl Hash for PdfRasterImage { fn hash(&self, state: &mut H) { // `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`, // so this is enough. Since `raster` is prehashed, this is also very cheap. @@ -105,7 +109,7 @@ impl Hash for PdfImage { } } -impl CustomImage for PdfImage { +impl CustomImage for PdfRasterImage { fn color_channel(&self) -> &[u8] { self.0 .actual_dynamic @@ -196,10 +200,15 @@ fn convert_raster( interpolate, ) } else { - krilla::image::Image::from_custom(PdfImage::new(raster), interpolate) + krilla::image::Image::from_custom(PdfRasterImage::new(raster), interpolate) } } +#[comemo::memoize] +fn convert_pdf(pdf: &PdfImage) -> PdfDocument { + PdfDocument::new(pdf.pdf().clone()) +} + fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) { let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| { if hp { diff --git a/crates/typst-pdf/src/link.rs b/crates/typst-pdf/src/link.rs index 64cb8f0a2..2133be82c 100644 --- a/crates/typst-pdf/src/link.rs +++ b/crates/typst-pdf/src/link.rs @@ -49,7 +49,6 @@ pub(crate) fn handle_link( fc.push_annotation( LinkAnnotation::new( rect, - None, Target::Action(Action::Link(LinkAction::new(u.to_string()))), ) .into(), @@ -64,7 +63,6 @@ pub(crate) fn handle_link( fc.push_annotation( LinkAnnotation::new( rect, - None, Target::Destination(krilla::destination::Destination::Named( nd.clone(), )), @@ -83,7 +81,6 @@ pub(crate) fn handle_link( fc.push_annotation( LinkAnnotation::new( rect, - None, Target::Destination(krilla::destination::Destination::Xyz( XyzDestination::new(index, pos.point.to_krilla()), )), diff --git a/crates/typst-render/Cargo.toml b/crates/typst-render/Cargo.toml index 7d01d7e38..5032344ee 100644 --- a/crates/typst-render/Cargo.toml +++ b/crates/typst-render/Cargo.toml @@ -18,6 +18,7 @@ typst-macros = { workspace = true } typst-timing = { workspace = true } bytemuck = { workspace = true } comemo = { workspace = true } +hayro = { workspace = true } image = { workspace = true } pixglyph = { workspace = true } resvg = { workspace = true } diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 7425bdd2f..83fd71eff 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -1,10 +1,12 @@ -use std::sync::Arc; - +use hayro::{FontData, FontQuery, InterpreterSettings, RenderSettings, StandardFont}; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; +use std::sync::Arc; use tiny_skia as sk; +use tiny_skia::IntSize; use typst_library::foundations::Smart; use typst_library::layout::Size; +use typst_library::text::{FontBook, FontStretch, FontStyle, FontVariant, FontWeight}; use typst_library::visualize::{Image, ImageKind, ImageScaling}; use crate::{AbsExt, State}; @@ -59,9 +61,9 @@ pub fn render_image( /// Prepare a texture for an image at a scaled size. #[comemo::memoize] fn build_texture(image: &Image, w: u32, h: u32) -> Option> { - let mut texture = sk::Pixmap::new(w, h)?; - match image.kind() { + let texture = match image.kind() { ImageKind::Raster(raster) => { + let mut texture = sk::Pixmap::new(w, h)?; let w = texture.width(); let h = texture.height(); @@ -85,15 +87,73 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option> { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } + + texture } ImageKind::Svg(svg) => { + let mut texture = sk::Pixmap::new(w, h)?; let tree = svg.tree(); let ts = tiny_skia::Transform::from_scale( w as f32 / tree.size().width(), h as f32 / tree.size().height(), ); resvg::render(tree, ts, &mut texture.as_mut()); + + texture } - } + ImageKind::Pdf(pdf) => { + let sf = pdf.standard_fonts().clone(); + + let select_standard_font = move |font: StandardFont| -> Option { + let bytes = match font { + StandardFont::Helvetica => sf.helvetica.normal.clone(), + StandardFont::HelveticaBold => sf.helvetica.bold.clone(), + StandardFont::HelveticaOblique => sf.helvetica.italic.clone(), + StandardFont::HelveticaBoldOblique => { + sf.helvetica.bold_italic.clone() + } + StandardFont::Courier => sf.courier.normal.clone(), + StandardFont::CourierBold => sf.courier.bold.clone(), + StandardFont::CourierOblique => sf.courier.italic.clone(), + StandardFont::CourierBoldOblique => sf.courier.bold_italic.clone(), + StandardFont::TimesRoman => sf.times.normal.clone(), + StandardFont::TimesBold => sf.times.bold.clone(), + StandardFont::TimesItalic => sf.times.italic.clone(), + StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), + StandardFont::Symbol => sf.symbol.clone(), + }; + + bytes.map(|d| { + let font_data: Arc + Send + Sync> = + Arc::new(d.clone()); + + font_data + }) + }; + + let interpreter_settings = InterpreterSettings { + font_resolver: Arc::new(move |query| match query { + FontQuery::Standard(s) => select_standard_font(*s), + FontQuery::Fallback(f) => { + select_standard_font(f.pick_standard_font()) + } + }), + warning_sink: Arc::new(|_| {}), + }; + let page = pdf.page(); + + let render_settings = RenderSettings { + x_scale: w as f32 / pdf.width(), + y_scale: h as f32 / pdf.height(), + width: Some(w as u16), + height: Some(h as u16), + }; + let hayro_pix = hayro::render(page, &interpreter_settings, &render_settings); + + sk::Pixmap::from_vec(hayro_pix.take_u8(), IntSize::from_wh(w, h)?)? + } + }; + Some(Arc::new(texture)) } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index fd4aecd4f..069e5b10a 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -66,6 +66,7 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { }), }, ImageKind::Svg(svg) => ("svg+xml", svg.data()), + ImageKind::Pdf(_) => todo!(), }; let mut url = eco_format!("data:image/{format};base64,"); From 9bacb89fc8f703e1756e6f74ace07deb2a5985c4 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sat, 19 Jul 2025 23:52:27 +0200 Subject: [PATCH 02/19] Extract pdf texture into a method --- Cargo.lock | 1 + crates/typst-render/src/image.rs | 109 ++++++++++++++++--------------- crates/typst-svg/Cargo.toml | 1 + 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d08dc9775..7b12838fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3258,6 +3258,7 @@ dependencies = [ "comemo", "ecow", "flate2", + "hayro", "image", "ttf-parser", "typst-library", diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 83fd71eff..89b87ce78 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -7,7 +7,7 @@ use tiny_skia::IntSize; use typst_library::foundations::Smart; use typst_library::layout::Size; use typst_library::text::{FontBook, FontStretch, FontStyle, FontVariant, FontWeight}; -use typst_library::visualize::{Image, ImageKind, ImageScaling}; +use typst_library::visualize::{Image, ImageKind, ImageScaling, PdfImage}; use crate::{AbsExt, State}; @@ -101,59 +101,62 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option> { texture } - ImageKind::Pdf(pdf) => { - let sf = pdf.standard_fonts().clone(); - - let select_standard_font = move |font: StandardFont| -> Option { - let bytes = match font { - StandardFont::Helvetica => sf.helvetica.normal.clone(), - StandardFont::HelveticaBold => sf.helvetica.bold.clone(), - StandardFont::HelveticaOblique => sf.helvetica.italic.clone(), - StandardFont::HelveticaBoldOblique => { - sf.helvetica.bold_italic.clone() - } - StandardFont::Courier => sf.courier.normal.clone(), - StandardFont::CourierBold => sf.courier.bold.clone(), - StandardFont::CourierOblique => sf.courier.italic.clone(), - StandardFont::CourierBoldOblique => sf.courier.bold_italic.clone(), - StandardFont::TimesRoman => sf.times.normal.clone(), - StandardFont::TimesBold => sf.times.bold.clone(), - StandardFont::TimesItalic => sf.times.italic.clone(), - StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), - StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), - StandardFont::Symbol => sf.symbol.clone(), - }; - - bytes.map(|d| { - let font_data: Arc + Send + Sync> = - Arc::new(d.clone()); - - font_data - }) - }; - - let interpreter_settings = InterpreterSettings { - font_resolver: Arc::new(move |query| match query { - FontQuery::Standard(s) => select_standard_font(*s), - FontQuery::Fallback(f) => { - select_standard_font(f.pick_standard_font()) - } - }), - warning_sink: Arc::new(|_| {}), - }; - let page = pdf.page(); - - let render_settings = RenderSettings { - x_scale: w as f32 / pdf.width(), - y_scale: h as f32 / pdf.height(), - width: Some(w as u16), - height: Some(h as u16), - }; - let hayro_pix = hayro::render(page, &interpreter_settings, &render_settings); - - sk::Pixmap::from_vec(hayro_pix.take_u8(), IntSize::from_wh(w, h)?)? - } + ImageKind::Pdf(pdf) => build_pdf_texture(pdf, w, h)?, }; Some(Arc::new(texture)) } + +fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { + let sf = pdf.standard_fonts().clone(); + + let select_standard_font = move |font: StandardFont| -> Option { + let bytes = match font { + StandardFont::Helvetica => sf.helvetica.normal.clone(), + StandardFont::HelveticaBold => sf.helvetica.bold.clone(), + StandardFont::HelveticaOblique => sf.helvetica.italic.clone(), + StandardFont::HelveticaBoldOblique => { + sf.helvetica.bold_italic.clone() + } + StandardFont::Courier => sf.courier.normal.clone(), + StandardFont::CourierBold => sf.courier.bold.clone(), + StandardFont::CourierOblique => sf.courier.italic.clone(), + StandardFont::CourierBoldOblique => sf.courier.bold_italic.clone(), + StandardFont::TimesRoman => sf.times.normal.clone(), + StandardFont::TimesBold => sf.times.bold.clone(), + StandardFont::TimesItalic => sf.times.italic.clone(), + StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), + StandardFont::Symbol => sf.symbol.clone(), + }; + + bytes.map(|d| { + let font_data: Arc + Send + Sync> = + Arc::new(d.clone()); + + font_data + }) + }; + + let interpreter_settings = InterpreterSettings { + font_resolver: Arc::new(move |query| match query { + FontQuery::Standard(s) => select_standard_font(*s), + FontQuery::Fallback(f) => { + select_standard_font(f.pick_standard_font()) + } + }), + warning_sink: Arc::new(|_| {}), + }; + let page = pdf.page(); + + let render_settings = RenderSettings { + x_scale: w as f32 / pdf.width(), + y_scale: h as f32 / pdf.height(), + width: Some(w as u16), + height: Some(h as u16), + }; + + let hayro_pix = hayro::render(page, &interpreter_settings, &render_settings); + + sk::Pixmap::from_vec(hayro_pix.take_u8(), IntSize::from_wh(w, h)?) +} diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml index 5416621e5..e1984d54e 100644 --- a/crates/typst-svg/Cargo.toml +++ b/crates/typst-svg/Cargo.toml @@ -21,6 +21,7 @@ base64 = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } flate2 = { workspace = true } +hayro = { workspace = true } image = { workspace = true } ttf-parser = { workspace = true } xmlparser = { workspace = true } From 070a375f3e04bf28b6bfe0005e45b14d76448724 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:02:55 +0200 Subject: [PATCH 03/19] Add SVG export --- crates/typst-render/src/image.rs | 1 + crates/typst-svg/src/image.rs | 92 +++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 89b87ce78..1e0ca5941 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -107,6 +107,7 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option> { Some(Arc::new(texture)) } +// Keep this in sync with `typst-svg`! fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { let sf = pdf.standard_fonts().clone(); diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 069e5b10a..d2400ac62 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,11 +1,12 @@ +use std::borrow::Cow; +use std::sync::Arc; use base64::Engine; use ecow::{eco_format, EcoString}; +use hayro::{FontData, FontQuery, InterpreterSettings, RenderSettings, StandardFont}; use image::{codecs::png::PngEncoder, ImageEncoder}; use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; -use typst_library::visualize::{ - ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, -}; +use typst_library::visualize::{ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat}; use crate::SVGRenderer; @@ -44,7 +45,7 @@ pub fn convert_image_scaling(scaling: Smart) -> Option<&'static st #[comemo::memoize] pub fn convert_image_to_base64_url(image: &Image) -> EcoString { let mut buf; - let (format, data): (&str, &[u8]) = match image.kind() { + let (format, data): (&str, Cow<[u8]>) = match image.kind() { ImageKind::Raster(raster) => match raster.format() { RasterFormat::Exchange(format) => ( match format { @@ -53,7 +54,7 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { ExchangeFormat::Gif => "gif", ExchangeFormat::Webp => "webp", }, - raster.data(), + Cow::Borrowed(raster.data()), ), RasterFormat::Pixel(_) => ("png", { buf = vec![]; @@ -62,11 +63,30 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { encoder.set_icc_profile(icc_profile.to_vec()).ok(); } raster.dynamic().write_with_encoder(encoder).unwrap(); - buf.as_slice() + Cow::Borrowed(buf.as_slice()) }), }, - ImageKind::Svg(svg) => ("svg+xml", svg.data()), - ImageKind::Pdf(_) => todo!(), + ImageKind::Svg(svg) => ("svg+xml", Cow::Borrowed(svg.data())), + ImageKind::Pdf(pdf) => { + // To make sure the image isn't pixelated, we always scale up so the lowest + // dimension has at least 1000 pixels. However, we also ensure that the largest dimension + // doesn't exceed 3000 pixels. + const MIN_WIDTH: f32 = 1000.0; + const MAX_WIDTH: f32 = 3000.0; + + + let base_width = pdf.width(); + let w_scale = (MIN_WIDTH / base_width).max(MAX_WIDTH / base_width); + let base_height = pdf.height(); + let h_scale = (MIN_WIDTH / base_height).min(MAX_WIDTH / base_height); + + let total_scale = w_scale.min(h_scale); + + let width = (base_width * total_scale).ceil() as u32; + let height = (base_height * total_scale).ceil() as u32; + + ("png", Cow::Owned(pdf_to_png(pdf, width, height))) + }, }; let mut url = eco_format!("data:image/{format};base64,"); @@ -74,3 +94,59 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { url.push_str(&data); url } + +// Keep this in sync with `typst-png`! +#[comemo::memoize] +fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { + let sf = pdf.standard_fonts().clone(); + + let select_standard_font = move |font: StandardFont| -> Option { + let bytes = match font { + StandardFont::Helvetica => sf.helvetica.normal.clone(), + StandardFont::HelveticaBold => sf.helvetica.bold.clone(), + StandardFont::HelveticaOblique => sf.helvetica.italic.clone(), + StandardFont::HelveticaBoldOblique => { + sf.helvetica.bold_italic.clone() + } + StandardFont::Courier => sf.courier.normal.clone(), + StandardFont::CourierBold => sf.courier.bold.clone(), + StandardFont::CourierOblique => sf.courier.italic.clone(), + StandardFont::CourierBoldOblique => sf.courier.bold_italic.clone(), + StandardFont::TimesRoman => sf.times.normal.clone(), + StandardFont::TimesBold => sf.times.bold.clone(), + StandardFont::TimesItalic => sf.times.italic.clone(), + StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), + StandardFont::Symbol => sf.symbol.clone(), + }; + + bytes.map(|d| { + let font_data: Arc + Send + Sync> = + Arc::new(d.clone()); + + font_data + }) + }; + + let interpreter_settings = InterpreterSettings { + font_resolver: Arc::new(move |query| match query { + FontQuery::Standard(s) => select_standard_font(*s), + FontQuery::Fallback(f) => { + select_standard_font(f.pick_standard_font()) + } + }), + warning_sink: Arc::new(|_| {}), + }; + let page = pdf.page(); + + let render_settings = RenderSettings { + x_scale: w as f32 / pdf.width(), + y_scale: h as f32 / pdf.height(), + width: Some(w as u16), + height: Some(h as u16), + }; + + let hayro_pix = hayro::render(page, &interpreter_settings, &render_settings); + + hayro_pix.take_png() +} \ No newline at end of file From 5ef332caa46f1bcb3ac4d63504cceda3900e9ffd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:13:25 +0200 Subject: [PATCH 04/19] Require the `PdfEmbedding` feature --- .../typst-library/src/visualize/image/mod.rs | 58 +++++++++++-------- crates/typst-render/src/image.rs | 13 ++--- crates/typst-svg/src/image.rs | 30 +++++----- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index e9b6df993..bbe6a5639 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -15,7 +15,7 @@ use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use ecow::{eco_format, EcoString}; -use typst_library::World; +use typst_library::{Feature, World}; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; @@ -271,32 +271,42 @@ impl Packed { .within(loaded)?, ), ImageFormat::Vector(VectorFormat::Pdf) => { - let document = - PdfDocument::new(loaded.data.clone(), engine.world.clone()) - .within(loaded)?; - let page_num = self.page.get(styles); + if engine.world.library().features.is_enabled(Feature::PdfEmbedding) { + let document = + PdfDocument::new(loaded.data.clone(), engine.world.clone()) + .within(loaded)?; + let page_num = self.page.get(styles); - if page_num == 0 { + if page_num == 0 { + bail!( + span, + "{page_num} is not a valid page number"; + hint: "page numbers for PDF start at 1" + ) + }; + + // The user provides the page number start from 1, further down the pipeline, + // page numbers are 0-based. + let page_idx = page_num - 1; + let num_pages = document.len(); + + let Some(pdf_image) = PdfImage::new(document, page_idx) else { + bail!( + span, + "page {page_num} doesn't exist"; + hint: "the document only has {num_pages} pages" + ); + }; + + ImageKind::Pdf(pdf_image) + } else { bail!( span, - "{page_num} is not a valid page number"; - hint: "page numbers start at 1" - ) - }; - - let page_idx = page_num - 1; - let num_pages = document.len(); - // The user provides the page number staring from 1, further down the pipeline they page - // numbers are 0-based. - let Some(pdf_image) = PdfImage::new(document, page_idx) else { - bail!( - span, - "page {page_num} doesn't exist"; - hint: "the document only has {num_pages} pages" - ) - }; - - ImageKind::Pdf(pdf_image) + "embedding PDFs is currently an experimental, opt-in feature"; + hint: "enable the corresponding feature to try it out"; + hint: "convert your PDF to SVG instead" + ); + } } }; diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 1e0ca5941..9ddc07ec8 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -116,9 +116,7 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { StandardFont::Helvetica => sf.helvetica.normal.clone(), StandardFont::HelveticaBold => sf.helvetica.bold.clone(), StandardFont::HelveticaOblique => sf.helvetica.italic.clone(), - StandardFont::HelveticaBoldOblique => { - sf.helvetica.bold_italic.clone() - } + StandardFont::HelveticaBoldOblique => sf.helvetica.bold_italic.clone(), StandardFont::Courier => sf.courier.normal.clone(), StandardFont::CourierBold => sf.courier.bold.clone(), StandardFont::CourierOblique => sf.courier.italic.clone(), @@ -132,8 +130,7 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { }; bytes.map(|d| { - let font_data: Arc + Send + Sync> = - Arc::new(d.clone()); + let font_data: Arc + Send + Sync> = Arc::new(d.clone()); font_data }) @@ -142,9 +139,7 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { let interpreter_settings = InterpreterSettings { font_resolver: Arc::new(move |query| match query { FontQuery::Standard(s) => select_standard_font(*s), - FontQuery::Fallback(f) => { - select_standard_font(f.pick_standard_font()) - } + FontQuery::Fallback(f) => select_standard_font(f.pick_standard_font()), }), warning_sink: Arc::new(|_| {}), }; @@ -156,7 +151,7 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { width: Some(w as u16), height: Some(h as u16), }; - + let hayro_pix = hayro::render(page, &interpreter_settings, &render_settings); sk::Pixmap::from_vec(hayro_pix.take_u8(), IntSize::from_wh(w, h)?) diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index d2400ac62..12a5683a5 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,12 +1,14 @@ -use std::borrow::Cow; -use std::sync::Arc; use base64::Engine; use ecow::{eco_format, EcoString}; use hayro::{FontData, FontQuery, InterpreterSettings, RenderSettings, StandardFont}; use image::{codecs::png::PngEncoder, ImageEncoder}; +use std::borrow::Cow; +use std::sync::Arc; use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; -use typst_library::visualize::{ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat}; +use typst_library::visualize::{ + ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, +}; use crate::SVGRenderer; @@ -73,20 +75,19 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { // doesn't exceed 3000 pixels. const MIN_WIDTH: f32 = 1000.0; const MAX_WIDTH: f32 = 3000.0; - - + let base_width = pdf.width(); let w_scale = (MIN_WIDTH / base_width).max(MAX_WIDTH / base_width); let base_height = pdf.height(); let h_scale = (MIN_WIDTH / base_height).min(MAX_WIDTH / base_height); - + let total_scale = w_scale.min(h_scale); - + let width = (base_width * total_scale).ceil() as u32; let height = (base_height * total_scale).ceil() as u32; ("png", Cow::Owned(pdf_to_png(pdf, width, height))) - }, + } }; let mut url = eco_format!("data:image/{format};base64,"); @@ -105,9 +106,7 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { StandardFont::Helvetica => sf.helvetica.normal.clone(), StandardFont::HelveticaBold => sf.helvetica.bold.clone(), StandardFont::HelveticaOblique => sf.helvetica.italic.clone(), - StandardFont::HelveticaBoldOblique => { - sf.helvetica.bold_italic.clone() - } + StandardFont::HelveticaBoldOblique => sf.helvetica.bold_italic.clone(), StandardFont::Courier => sf.courier.normal.clone(), StandardFont::CourierBold => sf.courier.bold.clone(), StandardFont::CourierOblique => sf.courier.italic.clone(), @@ -121,8 +120,7 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { }; bytes.map(|d| { - let font_data: Arc + Send + Sync> = - Arc::new(d.clone()); + let font_data: Arc + Send + Sync> = Arc::new(d.clone()); font_data }) @@ -131,9 +129,7 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { let interpreter_settings = InterpreterSettings { font_resolver: Arc::new(move |query| match query { FontQuery::Standard(s) => select_standard_font(*s), - FontQuery::Fallback(f) => { - select_standard_font(f.pick_standard_font()) - } + FontQuery::Fallback(f) => select_standard_font(f.pick_standard_font()), }), warning_sink: Arc::new(|_| {}), }; @@ -149,4 +145,4 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { let hayro_pix = hayro::render(page, &interpreter_settings, &render_settings); hayro_pix.take_png() -} \ No newline at end of file +} From a2df88db16f78f17fc71a8cad5d3a4894ab109d2 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:16:11 +0200 Subject: [PATCH 05/19] Fix some compiler warnings --- crates/typst-library/src/visualize/image/mod.rs | 4 ++-- crates/typst-library/src/visualize/image/pdf.rs | 2 +- crates/typst-render/src/image.rs | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index bbe6a5639..7a352db33 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -14,12 +14,12 @@ use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; -use ecow::{eco_format, EcoString}; +use ecow::EcoString; use typst_library::{Feature, World}; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{bail, error, warning, At, LoadedWithin, SourceResult, StrResult}; +use crate::diag::{bail, warning, At, LoadedWithin, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index 7f541e1e6..baad7e46f 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -1,4 +1,4 @@ -use crate::diag::{LoadError, LoadResult, SourceResult}; +use crate::diag::LoadResult; use crate::foundations::Bytes; use crate::text::{FontStretch, FontStyle, FontVariant, FontWeight}; use crate::World; diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 9ddc07ec8..a03df0d1d 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -6,7 +6,6 @@ use tiny_skia as sk; use tiny_skia::IntSize; use typst_library::foundations::Smart; use typst_library::layout::Size; -use typst_library::text::{FontBook, FontStretch, FontStyle, FontVariant, FontWeight}; use typst_library::visualize::{Image, ImageKind, ImageScaling, PdfImage}; use crate::{AbsExt, State}; From e9421fd16b5f1f0ed92f9cc381e7849e1e906162 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:32:34 +0200 Subject: [PATCH 06/19] Improve errors for PDF export --- crates/typst-pdf/src/convert.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 0849545e7..b27053e3c 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -11,6 +11,7 @@ use krilla::geom::PathBuilder; use krilla::page::{PageLabel, PageSettings}; use krilla::surface::Surface; use krilla::{Document, SerializeSettings}; +use krilla::pdf::PdfError; use krilla_svg::render_svg_glyph; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::foundations::{NativeElement, Repr}; @@ -364,9 +365,24 @@ fn finish( ) } KrillaError::Pdf(_, e, loc) => { - // TODO: Better errors + // TODO: prohibit export in PDF/A3, etc. let span = to_span(loc); - bail!(span, "failed to process PDF"); + match e { + // We already validated in `typst-library` that the page index is valid. + PdfError::InvalidPage(_) => unreachable!(), + PdfError::VersionMismatch(v) => { + let pdf_ver = v.as_str(); + let config_ver = configuration.version(); + let cur_ver = config_ver.as_str(); + + bail!(span, + "the version of the PDF file is too high"; + hint: "the current export target is {cur_ver}, while the PDF has version {pdf_ver}"; + hint: "raise the export target to {pdf_ver} or higher"; + hint: "preprocess the PDF to convert it to a lower version" + ); + } + } } KrillaError::DuplicateTagId(_, loc) => { let span = to_span(loc); @@ -595,7 +611,13 @@ fn convert_error( "{prefix} missing document date"; hint: "set the date of the document" ), - ValidationError::EmbeddedPDF(loc) => error!(to_span(*loc), "TODO"), + ValidationError::EmbeddedPDF(loc) => { + error!( + to_span(*loc), + "embedding PDFs is currently not supported in this export mode"; + hint: "try converting the PDF to SVG before embedding it" + ) + }, } } From 5427645c7a4e27c042e20f3c3f241c691987af27 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:33:43 +0200 Subject: [PATCH 07/19] Remove an outdated comment --- crates/typst-pdf/src/convert.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index b27053e3c..c214f8539 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -365,7 +365,6 @@ fn finish( ) } KrillaError::Pdf(_, e, loc) => { - // TODO: prohibit export in PDF/A3, etc. let span = to_span(loc); match e { // We already validated in `typst-library` that the page index is valid. From d19b45784c2e07c055eda6985035b08f0d3ac716 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:36:56 +0200 Subject: [PATCH 08/19] Reformat & rearrange imports --- crates/typst-pdf/src/convert.rs | 8 ++++---- crates/typst-svg/src/image.rs | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index c214f8539..71a49394a 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -9,9 +9,9 @@ use krilla::embed::EmbedError; use krilla::error::KrillaError; use krilla::geom::PathBuilder; use krilla::page::{PageLabel, PageSettings}; +use krilla::pdf::PdfError; use krilla::surface::Surface; use krilla::{Document, SerializeSettings}; -use krilla::pdf::PdfError; use krilla_svg::render_svg_glyph; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::foundations::{NativeElement, Repr}; @@ -373,8 +373,8 @@ fn finish( let pdf_ver = v.as_str(); let config_ver = configuration.version(); let cur_ver = config_ver.as_str(); - - bail!(span, + + bail!(span, "the version of the PDF file is too high"; hint: "the current export target is {cur_ver}, while the PDF has version {pdf_ver}"; hint: "raise the export target to {pdf_ver} or higher"; @@ -616,7 +616,7 @@ fn convert_error( "embedding PDFs is currently not supported in this export mode"; hint: "try converting the PDF to SVG before embedding it" ) - }, + } } } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 12a5683a5..81922dfe8 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,9 +1,10 @@ +use std::borrow::Cow; +use std::sync::Arc; + use base64::Engine; use ecow::{eco_format, EcoString}; use hayro::{FontData, FontQuery, InterpreterSettings, RenderSettings, StandardFont}; use image::{codecs::png::PngEncoder, ImageEncoder}; -use std::borrow::Cow; -use std::sync::Arc; use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; use typst_library::visualize::{ @@ -71,8 +72,8 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { ImageKind::Svg(svg) => ("svg+xml", Cow::Borrowed(svg.data())), ImageKind::Pdf(pdf) => { // To make sure the image isn't pixelated, we always scale up so the lowest - // dimension has at least 1000 pixels. However, we also ensure that the largest dimension - // doesn't exceed 3000 pixels. + // dimension has at least 1000 pixels. However, we only scale up as much so that the + // largest dimension doesn't exceed 3000 pixels. const MIN_WIDTH: f32 = 1000.0; const MAX_WIDTH: f32 = 3000.0; From f9862eba8d8bd2ed239240464a1a7721aacf96e7 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 00:40:11 +0200 Subject: [PATCH 09/19] Tweak resolution --- crates/typst-svg/src/image.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 81922dfe8..0674db090 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -74,13 +74,13 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { // To make sure the image isn't pixelated, we always scale up so the lowest // dimension has at least 1000 pixels. However, we only scale up as much so that the // largest dimension doesn't exceed 3000 pixels. - const MIN_WIDTH: f32 = 1000.0; - const MAX_WIDTH: f32 = 3000.0; + const MIN_RES: f32 = 1000.0; + const MAX_RES: f32 = 3000.0; let base_width = pdf.width(); - let w_scale = (MIN_WIDTH / base_width).max(MAX_WIDTH / base_width); + let w_scale = (MIN_RES / base_width).max(MAX_RES / base_width); let base_height = pdf.height(); - let h_scale = (MIN_WIDTH / base_height).min(MAX_WIDTH / base_height); + let h_scale = (MIN_RES / base_height).min(MAX_RES / base_height); let total_scale = w_scale.min(h_scale); From c384f6b6c05006616516bb5b6773ba495396ba30 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 10:06:33 +0200 Subject: [PATCH 10/19] Use `select_fallback` as well --- crates/typst-library/src/visualize/image/pdf.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index baad7e46f..a7e3f7ce4 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -7,6 +7,7 @@ use hayro_syntax::page::Page; use hayro_syntax::Pdf; use std::hash::{Hash, Hasher}; use std::sync::Arc; +use typst_library::text::FontInfo; struct DocumentRepr { pdf: Arc, @@ -121,6 +122,9 @@ fn get_standard_fonts(world: Tracked) -> Arc { None } }) + .or_else(|| { + book.select_fallback(None, variant, "A") + }) .and_then(|i| world.font(i)) .map(|font| font.data().clone()) }; From 1a66c8ebe731946899a12d426ba180cc7826fe8e Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 10:52:12 +0200 Subject: [PATCH 11/19] Bump deps --- Cargo.lock | 15 +++++++-------- Cargo.toml | 10 +++++----- .../typst-library/src/visualize/image/pdf.rs | 19 ++++++++----------- crates/typst-render/src/image.rs | 10 +++++----- crates/typst-svg/src/image.rs | 10 +++++----- 5 files changed, 30 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b12838fe..35b0d6566 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -967,7 +967,7 @@ dependencies = [ [[package]] name = "hayro" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" dependencies = [ "bytemuck", "hayro-interpret", @@ -980,7 +980,7 @@ dependencies = [ [[package]] name = "hayro-font" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" dependencies = [ "log", "phf", @@ -989,7 +989,7 @@ dependencies = [ [[package]] name = "hayro-interpret" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" dependencies = [ "bitflags 2.9.1", "hayro-font", @@ -1006,7 +1006,7 @@ dependencies = [ [[package]] name = "hayro-syntax" version = "0.0.1" -source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" dependencies = [ "flate2", "kurbo", @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "hayro-write" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=d385360#d38536089c95b521b0b64080302d7a305344edbf" +source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" dependencies = [ "flate2", "hayro-syntax", @@ -1430,7 +1430,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.4.0" -source = "git+https://github.com/LaurenzV/krilla?rev=f7d753c3#f7d753c39e1fb7118fb1b774243a0720771f229f" +source = "git+https://github.com/LaurenzV/krilla?rev=1668ac2#1668ac2e64dc85572e6c62a2399e85acd39b619d" dependencies = [ "base64", "bumpalo", @@ -1460,7 +1460,7 @@ dependencies = [ [[package]] name = "krilla-svg" version = "0.1.0" -source = "git+https://github.com/LaurenzV/krilla?rev=f7d753c3#f7d753c39e1fb7118fb1b774243a0720771f229f" +source = "git+https://github.com/LaurenzV/krilla?rev=1668ac2#1668ac2e64dc85572e6c62a2399e85acd39b619d" dependencies = [ "flate2", "fontdb", @@ -2926,7 +2926,6 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index 07ba7fb0a..99e84ad23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" } +typst-assets = { path = "../typst-assets" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } arrayvec = "0.7.4" az = "1.2" @@ -61,8 +61,8 @@ fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" glidesort = "0.1.2" hayagriva = "0.8.1" -hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "d385360" } -hayro = { git = "https://github.com/LaurenzV/hayro", rev = "d385360" } +hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "2b63dc8" } +hayro = { git = "https://github.com/LaurenzV/hayro", rev = "2b63dc8" } heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -75,8 +75,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" -krilla = { git = "https://github.com/LaurenzV/krilla", rev = "f7d753c3", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } -krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "f7d753c3" } +krilla = { git = "https://github.com/LaurenzV/krilla", rev = "1668ac2", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "1668ac2" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index a7e3f7ce4..cac03a40f 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -122,11 +122,9 @@ fn get_standard_fonts(world: Tracked) -> Arc { None } }) - .or_else(|| { - book.select_fallback(None, variant, "A") - }) + .or_else(|| book.select_fallback(None, variant, "A")) .and_then(|i| world.font(i)) - .map(|font| font.data().clone()) + .map(|font| (font.data().clone(), font.index())) }; let normal_variant = FontVariant::new( @@ -165,18 +163,17 @@ fn get_standard_fonts(world: Tracked) -> Arc { bold_italic: get_font("times", Some("liberation serif"), bold_italic_variant), }; - // TODO: Use Foxit fonts as fallback - let symbol = get_font("symbol", None, normal_variant); - let zapf_dingbats = get_font("zapf dingbats", None, normal_variant); + let symbol = Some(Bytes::new(typst_assets::pdf::SYMBOL)); + let zapf_dingbats = Some(Bytes::new(typst_assets::pdf::DING_BATS)); Arc::new(StandardFonts { helvetica, courier, times, symbol, zapf_dingbats }) } pub struct VariantFont { - pub normal: Option, - pub bold: Option, - pub italic: Option, - pub bold_italic: Option, + pub normal: Option<(Bytes, u32)>, + pub bold: Option<(Bytes, u32)>, + pub italic: Option<(Bytes, u32)>, + pub bold_italic: Option<(Bytes, u32)>, } pub struct StandardFonts { diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index a03df0d1d..96f164412 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -110,7 +110,7 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option> { fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { let sf = pdf.standard_fonts().clone(); - let select_standard_font = move |font: StandardFont| -> Option { + let select_standard_font = move |font: StandardFont| -> Option<(FontData, u32)> { let bytes = match font { StandardFont::Helvetica => sf.helvetica.normal.clone(), StandardFont::HelveticaBold => sf.helvetica.bold.clone(), @@ -124,14 +124,14 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { StandardFont::TimesBold => sf.times.bold.clone(), StandardFont::TimesItalic => sf.times.italic.clone(), StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), - StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), - StandardFont::Symbol => sf.symbol.clone(), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone().map(|d| (d, 0)), + StandardFont::Symbol => sf.symbol.clone().map(|d| (d, 0)), }; bytes.map(|d| { - let font_data: Arc + Send + Sync> = Arc::new(d.clone()); + let font_data: Arc + Send + Sync> = Arc::new(d.0.clone()); - font_data + (font_data, d.1) }) }; diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 0674db090..82d7f7f13 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -102,7 +102,7 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { let sf = pdf.standard_fonts().clone(); - let select_standard_font = move |font: StandardFont| -> Option { + let select_standard_font = move |font: StandardFont| -> Option<(FontData, u32)> { let bytes = match font { StandardFont::Helvetica => sf.helvetica.normal.clone(), StandardFont::HelveticaBold => sf.helvetica.bold.clone(), @@ -116,14 +116,14 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { StandardFont::TimesBold => sf.times.bold.clone(), StandardFont::TimesItalic => sf.times.italic.clone(), StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), - StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), - StandardFont::Symbol => sf.symbol.clone(), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone().map(|d| (d, 0)), + StandardFont::Symbol => sf.symbol.clone().map(|d| (d, 0)), }; bytes.map(|d| { - let font_data: Arc + Send + Sync> = Arc::new(d.clone()); + let font_data: Arc + Send + Sync> = Arc::new(d.0.clone()); - font_data + (font_data, d.1) }) }; From d9fb17edb3dedf77435d8dd867f838ca5a3d57cd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 14:30:08 +0200 Subject: [PATCH 12/19] Embed all fonts --- .../typst-library/src/visualize/image/mod.rs | 3 +- .../typst-library/src/visualize/image/pdf.rs | 82 ++++++------------- crates/typst-render/src/image.rs | 10 +-- crates/typst-svg/src/image.rs | 10 +-- 4 files changed, 32 insertions(+), 73 deletions(-) diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 7a352db33..fc315bfec 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -273,8 +273,7 @@ impl Packed { ImageFormat::Vector(VectorFormat::Pdf) => { if engine.world.library().features.is_enabled(Feature::PdfEmbedding) { let document = - PdfDocument::new(loaded.data.clone(), engine.world.clone()) - .within(loaded)?; + PdfDocument::new(loaded.data.clone()).within(loaded)?; let page_num = self.page.get(styles); if page_num == 0 { diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index cac03a40f..112e1138b 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -1,13 +1,9 @@ use crate::diag::LoadResult; use crate::foundations::Bytes; -use crate::text::{FontStretch, FontStyle, FontVariant, FontWeight}; -use crate::World; -use comemo::Tracked; use hayro_syntax::page::Page; use hayro_syntax::Pdf; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use typst_library::text::FontInfo; struct DocumentRepr { pdf: Arc, @@ -29,10 +25,10 @@ impl PdfDocument { /// Load a PDF document. #[comemo::memoize] #[typst_macros::time(name = "load pdf document")] - pub fn new(data: Bytes, world: Tracked) -> LoadResult { + pub fn new(data: Bytes) -> LoadResult { // TODO: Remove unwraps let pdf = Arc::new(Pdf::new(Arc::new(data.clone())).unwrap()); - let standard_fonts = get_standard_fonts(world.clone()); + let standard_fonts = get_standard_fonts(); Ok(Self(Arc::new(DocumentRepr { data, pdf, standard_fonts }))) } @@ -64,7 +60,6 @@ impl PdfImage { /// Create a new PDF image. Returns `None` if the page index is not valid. #[comemo::memoize] pub fn new(document: PdfDocument, page: usize) -> Option { - // TODO: Don't allow loading if pdf-embedding feature is disabled. // TODO: Remove Unwrap let dimensions = document.0.pdf.pages().get(page)?.render_dimensions(); @@ -110,76 +105,45 @@ impl PdfImage { } #[comemo::memoize] -fn get_standard_fonts(world: Tracked) -> Arc { - let book = world.book(); - - let get_font = |name: &str, fallback_name: Option<&str>, variant: FontVariant| { - book.select(name, variant) - .or_else(|| { - if let Some(fallback_name) = fallback_name { - book.select(fallback_name, variant) - } else { - None - } - }) - .or_else(|| book.select_fallback(None, variant, "A")) - .and_then(|i| world.font(i)) - .map(|font| (font.data().clone(), font.index())) - }; - - let normal_variant = FontVariant::new( - FontStyle::Normal, - FontWeight::default(), - FontStretch::default(), - ); - let bold_variant = - FontVariant::new(FontStyle::Normal, FontWeight::BOLD, FontStretch::default()); - let italic_variant = FontVariant::new( - FontStyle::Italic, - FontWeight::default(), - FontStretch::default(), - ); - let bold_italic_variant = - FontVariant::new(FontStyle::Italic, FontWeight::BOLD, FontStretch::default()); - +fn get_standard_fonts() -> Arc { let helvetica = VariantFont { - normal: get_font("helvetica", Some("liberation sans"), normal_variant), - bold: get_font("helvetica", Some("liberation sans"), bold_variant), - italic: get_font("helvetica", Some("liberation sans"), italic_variant), - bold_italic: get_font("helvetica", Some("liberation sans"), bold_italic_variant), + normal: Bytes::new(typst_assets::pdf::SANS), + bold: Bytes::new(typst_assets::pdf::SANS_BOLD), + italic: Bytes::new(typst_assets::pdf::SANS_ITALIC), + bold_italic: Bytes::new(typst_assets::pdf::SANS_BOLD_ITALIC), }; let courier = VariantFont { - normal: get_font("courier", Some("liberation mono"), normal_variant), - bold: get_font("courier", Some("liberation mono"), bold_variant), - italic: get_font("courier", Some("liberation mono"), italic_variant), - bold_italic: get_font("courier", Some("liberation mono"), bold_italic_variant), + normal: Bytes::new(typst_assets::pdf::FIXED), + bold: Bytes::new(typst_assets::pdf::FIXED_BOLD), + italic: Bytes::new(typst_assets::pdf::FIXED_ITALIC), + bold_italic: Bytes::new(typst_assets::pdf::FIXED_BOLD_ITALIC), }; let times = VariantFont { - normal: get_font("times", Some("liberation serif"), normal_variant), - bold: get_font("times", Some("liberation serif"), bold_variant), - italic: get_font("times", Some("liberation serif"), italic_variant), - bold_italic: get_font("times", Some("liberation serif"), bold_italic_variant), + normal: Bytes::new(typst_assets::pdf::SERIF), + bold: Bytes::new(typst_assets::pdf::SERIF_BOLD), + italic: Bytes::new(typst_assets::pdf::SERIF_ITALIC), + bold_italic: Bytes::new(typst_assets::pdf::SERIF_BOLD_ITALIC), }; - let symbol = Some(Bytes::new(typst_assets::pdf::SYMBOL)); - let zapf_dingbats = Some(Bytes::new(typst_assets::pdf::DING_BATS)); + let symbol = Bytes::new(typst_assets::pdf::SYMBOL); + let zapf_dingbats = Bytes::new(typst_assets::pdf::DING_BATS); Arc::new(StandardFonts { helvetica, courier, times, symbol, zapf_dingbats }) } pub struct VariantFont { - pub normal: Option<(Bytes, u32)>, - pub bold: Option<(Bytes, u32)>, - pub italic: Option<(Bytes, u32)>, - pub bold_italic: Option<(Bytes, u32)>, + pub normal: Bytes, + pub bold: Bytes, + pub italic: Bytes, + pub bold_italic: Bytes, } pub struct StandardFonts { pub helvetica: VariantFont, pub courier: VariantFont, pub times: VariantFont, - pub symbol: Option, - pub zapf_dingbats: Option, + pub symbol: Bytes, + pub zapf_dingbats: Bytes, } diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 96f164412..7385794a8 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -124,15 +124,13 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option { StandardFont::TimesBold => sf.times.bold.clone(), StandardFont::TimesItalic => sf.times.italic.clone(), StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), - StandardFont::ZapfDingBats => sf.zapf_dingbats.clone().map(|d| (d, 0)), - StandardFont::Symbol => sf.symbol.clone().map(|d| (d, 0)), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), + StandardFont::Symbol => sf.symbol.clone(), }; - bytes.map(|d| { - let font_data: Arc + Send + Sync> = Arc::new(d.0.clone()); + let font_data: Arc + Send + Sync> = Arc::new(bytes.clone()); - (font_data, d.1) - }) + Some((font_data, 0)) }; let interpreter_settings = InterpreterSettings { diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 82d7f7f13..40a982462 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -116,15 +116,13 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec { StandardFont::TimesBold => sf.times.bold.clone(), StandardFont::TimesItalic => sf.times.italic.clone(), StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(), - StandardFont::ZapfDingBats => sf.zapf_dingbats.clone().map(|d| (d, 0)), - StandardFont::Symbol => sf.symbol.clone().map(|d| (d, 0)), + StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(), + StandardFont::Symbol => sf.symbol.clone(), }; - bytes.map(|d| { - let font_data: Arc + Send + Sync> = Arc::new(d.0.clone()); + let font_data: Arc + Send + Sync> = Arc::new(bytes.clone()); - (font_data, d.1) - }) + Some((font_data, 0)) }; let interpreter_settings = InterpreterSettings { From 62daf7d3ef5aef241d94c80084c89b2c619f4d5d Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 14:40:14 +0200 Subject: [PATCH 13/19] Add proper errors for PDFs that failed to load --- .../typst-library/src/visualize/image/mod.rs | 26 ++++++++++++++++--- .../typst-library/src/visualize/image/pdf.rs | 8 +++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index fc315bfec..399cca715 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -15,6 +15,7 @@ use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use ecow::EcoString; +use hayro_syntax::LoadPdfError; use typst_library::{Feature, World}; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; @@ -272,8 +273,27 @@ impl Packed { ), ImageFormat::Vector(VectorFormat::Pdf) => { if engine.world.library().features.is_enabled(Feature::PdfEmbedding) { - let document = - PdfDocument::new(loaded.data.clone()).within(loaded)?; + let document = match PdfDocument::new(loaded.data.clone()) { + Ok(doc) => doc, + Err(e) => match e { + LoadPdfError::Encryption => { + bail!( + span, + "the PDF is encrypted or password-protected"; + hint: "such PDFs are currently not supported"; + hint: "preprocess the PDF to remove the encryption" + ); + } + LoadPdfError::Invalid => { + bail!( + span, + "the PDF could not be loaded"; + hint: "perhaps the PDF file is malformed" + ); + } + }, + }; + let page_num = self.page.get(styles); if page_num == 0 { @@ -284,7 +304,7 @@ impl Packed { ) }; - // The user provides the page number start from 1, further down the pipeline, + // The user provides the page number start from 1, but further down the pipeline, // page numbers are 0-based. let page_idx = page_num - 1; let num_pages = document.len(); diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index 112e1138b..f4bee702f 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -1,7 +1,6 @@ -use crate::diag::LoadResult; use crate::foundations::Bytes; use hayro_syntax::page::Page; -use hayro_syntax::Pdf; +use hayro_syntax::{LoadPdfError, Pdf}; use std::hash::{Hash, Hasher}; use std::sync::Arc; @@ -25,9 +24,8 @@ impl PdfDocument { /// Load a PDF document. #[comemo::memoize] #[typst_macros::time(name = "load pdf document")] - pub fn new(data: Bytes) -> LoadResult { - // TODO: Remove unwraps - let pdf = Arc::new(Pdf::new(Arc::new(data.clone())).unwrap()); + pub fn new(data: Bytes) -> Result { + let pdf = Arc::new(Pdf::new(Arc::new(data.clone()))?); let standard_fonts = get_standard_fonts(); Ok(Self(Arc::new(DocumentRepr { data, pdf, standard_fonts }))) From 24d68ba61c69f955b763eec0f6fd630115996ae2 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 14:48:50 +0200 Subject: [PATCH 14/19] Update documentation --- .../typst-library/src/visualize/image/pdf.rs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index f4bee702f..ce657b2c9 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -31,16 +31,17 @@ impl PdfDocument { Ok(Self(Arc::new(DocumentRepr { data, pdf, standard_fonts }))) } + /// Return the number of pages in the PDF. pub fn len(&self) -> usize { self.0.pdf.pages().len() } } struct ImageRepr { - pub document: PdfDocument, - pub page_index: usize, - pub width: f32, - pub height: f32, + document: PdfDocument, + page_index: usize, + width: f32, + height: f32, } impl Hash for ImageRepr { @@ -50,12 +51,14 @@ impl Hash for ImageRepr { } } -/// A page of a PDF file. +/// A specific page of a PDF acting as an image. #[derive(Clone, Hash)] pub struct PdfImage(Arc); impl PdfImage { - /// Create a new PDF image. Returns `None` if the page index is not valid. + /// Create a new PDF image. + /// + /// Returns `None` if the page index is not valid. #[comemo::memoize] pub fn new(document: PdfDocument, page: usize) -> Option { // TODO: Remove Unwrap @@ -69,34 +72,37 @@ impl PdfImage { }))) } + /// Returns the PDF page of the image. pub fn page(&self) -> &Page { - &self.0.document.0.pdf.pages()[self.0.page_index] + &self.pdf().pages()[self.0.page_index] } + /// Returns the underlying PDF document. pub fn pdf(&self) -> &Arc { &self.0.document.0.pdf } + /// Returns the width of the image. pub fn width(&self) -> f32 { self.0.width } + /// Returns the embedded standard fonts of the image. pub fn standard_fonts(&self) -> &Arc { &self.0.document.0.standard_fonts } + /// Returns the height of the image. pub fn height(&self) -> f32 { self.0.height } - pub fn data(&self) -> &Bytes { - &self.0.document.0.data - } - + /// Returns the page index of the image. pub fn page_index(&self) -> usize { self.0.page_index } + /// Returns the underlying Typst PDF document. pub fn document(&self) -> &PdfDocument { &self.0.document } @@ -131,17 +137,28 @@ fn get_standard_fonts() -> Arc { Arc::new(StandardFonts { helvetica, courier, times, symbol, zapf_dingbats }) } +/// A PDF font with multiple variants. pub struct VariantFont { + /// The normal variant. pub normal: Bytes, + /// The bold variant. pub bold: Bytes, + /// The italic variant. pub italic: Bytes, + /// The bold-italic variant. pub bold_italic: Bytes, } +/// A structure holding the raw data of all PDF standard fonts. pub struct StandardFonts { + /// The data for the `Helvetica` font family. pub helvetica: VariantFont, + /// The data for the `Courier` font family. pub courier: VariantFont, + /// The data for the `Times` font family. pub times: VariantFont, + /// The data for the `Symbol` font family. pub symbol: Bytes, + /// The data for the `Zapf Dingbats` font family. pub zapf_dingbats: Bytes, } From 77911624554d650add3d51182d0949e8f885c1f5 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 14:49:34 +0200 Subject: [PATCH 15/19] Group imports --- crates/typst-library/src/visualize/image/pdf.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index ce657b2c9..970096b03 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -1,9 +1,11 @@ -use crate::foundations::Bytes; -use hayro_syntax::page::Page; -use hayro_syntax::{LoadPdfError, Pdf}; use std::hash::{Hash, Hasher}; use std::sync::Arc; +use hayro_syntax::page::Page; +use hayro_syntax::{LoadPdfError, Pdf}; + +use crate::foundations::Bytes; + struct DocumentRepr { pdf: Arc, data: Bytes, From 62eab1dc9f0bfad55828f487745405157e843208 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 14:50:02 +0200 Subject: [PATCH 16/19] Remove outdated comment --- crates/typst-library/src/visualize/image/pdf.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index 970096b03..6bf446e06 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -63,7 +63,6 @@ impl PdfImage { /// Returns `None` if the page index is not valid. #[comemo::memoize] pub fn new(document: PdfDocument, page: usize) -> Option { - // TODO: Remove Unwrap let dimensions = document.0.pdf.pages().get(page)?.render_dimensions(); Some(Self(Arc::new(ImageRepr { From 2f72a1f1971ca4a5cb8ed8111d2b000ff45aa68d Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 15:13:34 +0200 Subject: [PATCH 17/19] Add tests --- crates/typst-library/src/visualize/image/mod.rs | 4 +++- crates/typst-library/src/visualize/image/pdf.rs | 4 ++-- tests/ref/image-multiple-pages.png | Bin 0 -> 7618 bytes tests/ref/image-pdf.png | Bin 0 -> 3987 bytes tests/src/world.rs | 2 +- tests/suite/visualize/image.typ | 15 ++++++++++++++- 6 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 tests/ref/image-multiple-pages.png create mode 100644 tests/ref/image-pdf.png diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 399cca715..26a486089 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -310,10 +310,12 @@ impl Packed { let num_pages = document.len(); let Some(pdf_image) = PdfImage::new(document, page_idx) else { + let pages = if num_pages == 1 { "page" } else { "pages" }; + bail!( span, "page {page_num} doesn't exist"; - hint: "the document only has {num_pages} pages" + hint: "the document only has {num_pages} {pages}" ); }; diff --git a/crates/typst-library/src/visualize/image/pdf.rs b/crates/typst-library/src/visualize/image/pdf.rs index 6bf446e06..c4d6c4f18 100644 --- a/crates/typst-library/src/visualize/image/pdf.rs +++ b/crates/typst-library/src/visualize/image/pdf.rs @@ -58,8 +58,8 @@ impl Hash for ImageRepr { pub struct PdfImage(Arc); impl PdfImage { - /// Create a new PDF image. - /// + /// Create a new PDF image. + /// /// Returns `None` if the page index is not valid. #[comemo::memoize] pub fn new(document: PdfDocument, page: usize) -> Option { diff --git a/tests/ref/image-multiple-pages.png b/tests/ref/image-multiple-pages.png new file mode 100644 index 0000000000000000000000000000000000000000..a6be4a31391ab97054623ceaf5f702f033f724a2 GIT binary patch literal 7618 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 literal 0 HcmV?d00001 diff --git a/tests/src/world.rs b/tests/src/world.rs index 4b6cf5a34..0c37b7bed 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -198,7 +198,7 @@ fn library() -> Library { // exactly 100pt wide. Page height is unbounded and font size is 10pt so // that it multiplies to nice round numbers. let mut lib = Library::builder() - .with_features([Feature::Html].into_iter().collect()) + .with_features([Feature::Html, Feature::PdfEmbedding].into_iter().collect()) .build(); // Hook up helpers into the global scope. diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 36ec06cb1..ca13526f9 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -258,7 +258,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-png-but-pixmap-format --- #image( read("/assets/images/tiger.jpg", encoding: none), - // Error: 11-18 expected "png", "jpg", "gif", "webp", dictionary, "svg", or auto + // Error: 11-18 expected "png", "jpg", "gif", "webp", dictionary, "svg", "pdf", or auto format: "rgba8", ) @@ -289,3 +289,16 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ..rotations.map(v => raw(str(v), lang: "typc")), ..rotations.map(rotated) ) + +--- image-pdf --- +#image("/assets/images/matplotlib.pdf") + +--- image-pdf-invalid-page --- +// Error: 2-49 page 2 doesn't exist +// Hint: 2-49 the document only has 1 page +#image("/assets/images/matplotlib.pdf", page: 2) + +--- image-multiple-pages --- +#image("/assets/images/diagrams.pdf", page: 1) +#image("/assets/images/diagrams.pdf", page: 3) +#image("/assets/images/diagrams.pdf", page: 2) \ No newline at end of file From e71057536be743ea6496ef383ca0c10ccd72c8d2 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 15:22:08 +0200 Subject: [PATCH 18/19] Rename test --- ...tiple-pages.png => image-pdf-multiple-pages.png} | Bin tests/suite/visualize/image.typ | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/ref/{image-multiple-pages.png => image-pdf-multiple-pages.png} (100%) diff --git a/tests/ref/image-multiple-pages.png b/tests/ref/image-pdf-multiple-pages.png similarity index 100% rename from tests/ref/image-multiple-pages.png rename to tests/ref/image-pdf-multiple-pages.png diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index ca13526f9..cb192d4bb 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -298,7 +298,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B // Hint: 2-49 the document only has 1 page #image("/assets/images/matplotlib.pdf", page: 2) ---- image-multiple-pages --- +--- image-pdf-multiple-pages --- #image("/assets/images/diagrams.pdf", page: 1) #image("/assets/images/diagrams.pdf", page: 3) #image("/assets/images/diagrams.pdf", page: 2) \ No newline at end of file From 77c25aed746e106b034406435c702d4a875952af Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sun, 20 Jul 2025 15:23:34 +0200 Subject: [PATCH 19/19] Update dependencies --- Cargo.lock | 17 +++++++++-------- Cargo.toml | 12 ++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35b0d6566..c3fd68af5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -967,7 +967,7 @@ dependencies = [ [[package]] name = "hayro" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" +source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d" dependencies = [ "bytemuck", "hayro-interpret", @@ -980,7 +980,7 @@ dependencies = [ [[package]] name = "hayro-font" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" +source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d" dependencies = [ "log", "phf", @@ -989,7 +989,7 @@ dependencies = [ [[package]] name = "hayro-interpret" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" +source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d" dependencies = [ "bitflags 2.9.1", "hayro-font", @@ -1006,7 +1006,7 @@ dependencies = [ [[package]] name = "hayro-syntax" version = "0.0.1" -source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" +source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d" dependencies = [ "flate2", "kurbo", @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "hayro-write" version = "0.1.0" -source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e" +source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d" dependencies = [ "flate2", "hayro-syntax", @@ -1430,7 +1430,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.4.0" -source = "git+https://github.com/LaurenzV/krilla?rev=1668ac2#1668ac2e64dc85572e6c62a2399e85acd39b619d" +source = "git+https://github.com/LaurenzV/krilla/?rev=2da9d6c#2da9d6c6cb6a6baa6379592c72ae4eb7e16aa7b4" dependencies = [ "base64", "bumpalo", @@ -1460,7 +1460,7 @@ dependencies = [ [[package]] name = "krilla-svg" version = "0.1.0" -source = "git+https://github.com/LaurenzV/krilla?rev=1668ac2#1668ac2e64dc85572e6c62a2399e85acd39b619d" +source = "git+https://github.com/LaurenzV/krilla/?rev=2da9d6c#2da9d6c6cb6a6baa6379592c72ae4eb7e16aa7b4" dependencies = [ "flate2", "fontdb", @@ -2926,6 +2926,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" +source = "git+https://github.com/LaurenzV/typst-assets?rev=d89cc82#d89cc821f84a5667714491019c0b64087b2608bd" [[package]] name = "typst-cli" @@ -2975,7 +2976,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648" +source = "git+https://github.com/LaurenzV/typst-dev-assets?rev=180c145#180c145cf810c8b2a7ed77355b351f3aed07d0c6" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index 99e84ad23..ff2c2acaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,8 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { path = "../typst-assets" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } +typst-assets = { git = "https://github.com/LaurenzV/typst-assets", rev = "d89cc82" } +typst-dev-assets = { git = "https://github.com/LaurenzV/typst-dev-assets", rev = "180c145" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" @@ -61,8 +61,8 @@ fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" glidesort = "0.1.2" hayagriva = "0.8.1" -hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "2b63dc8" } -hayro = { git = "https://github.com/LaurenzV/hayro", rev = "2b63dc8" } +hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "d651e18" } +hayro = { git = "https://github.com/LaurenzV/hayro", rev = "d651e18" } heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -75,8 +75,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" -krilla = { git = "https://github.com/LaurenzV/krilla", rev = "1668ac2", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } -krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "1668ac2" } +krilla = { git = "https://github.com/LaurenzV/krilla/", rev = "2da9d6c", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla/", rev = "2da9d6c"} kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9"