Merge f9862eba8d8bd2ed239240464a1a7721aacf96e7 into b790c6d59ceaf7a809cc24b60c1f1509807470e2

This commit is contained in:
Laurenz Stampfl 2025-07-19 22:40:17 +00:00 committed by GitHub
commit b0416a4cc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 557 additions and 52 deletions

View File

@ -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

118
Cargo.lock generated
View File

@ -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",
@ -3191,6 +3258,7 @@ dependencies = [
"comemo",
"ecow",
"flate2",
"hayro",
"image",
"ttf-parser",
"typst-library",
@ -3589,7 +3657,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 +3792,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]]

View File

@ -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"

View File

@ -471,6 +471,7 @@ display_possible_values!(DiagnosticFormat);
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum Feature {
Html,
PdfEmbedding,
}
display_possible_values!(Feature);

View File

@ -117,6 +117,7 @@ impl SystemWorld {
.iter()
.map(|&feature| match feature {
Feature::Html => typst::Feature::Html,
Feature::PdfEmbedding => typst::Feature::PdfEmbedding,
})
.collect();

View File

@ -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 }

View File

@ -237,6 +237,7 @@ impl FromIterator<Feature> for Features {
#[non_exhaustive]
pub enum Feature {
Html,
PdfEmbedding,
}
/// A group of related standard library definitions.

View File

@ -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,
};
@ -13,10 +15,11 @@ use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use ecow::EcoString;
use typst_library::{Feature, World};
use typst_syntax::{Span, Spanned};
use typst_utils::LazyHash;
use crate::diag::{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,
@ -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<EcoString>,
/// 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,44 @@ impl Packed<ImageElem> {
)
.within(loaded)?,
),
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)?;
let page_num = self.page.get(styles);
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,
"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"
);
}
}
};
Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles)))
@ -286,6 +333,7 @@ impl Packed<ImageElem> {
"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 +421,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 +430,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 +439,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 +448,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 +487,8 @@ pub enum ImageKind {
Raster(RasterImage),
/// An SVG image.
Svg(SvgImage),
/// A PDF image.
Pdf(PdfImage),
}
impl From<RasterImage> for ImageKind {
@ -469,10 +523,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 +557,8 @@ fn is_svg(data: &[u8]) -> bool {
pub enum VectorFormat {
/// The vector graphics format of the web.
Svg,
/// The PDF graphics format.
Pdf,
}
impl<R> From<R> for ImageFormat

View File

@ -0,0 +1,184 @@
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;
struct DocumentRepr {
pdf: Arc<Pdf>,
data: Bytes,
standard_fonts: Arc<StandardFonts>,
}
impl Hash for DocumentRepr {
fn hash<H: Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
/// A PDF document.
#[derive(Clone, Hash)]
pub struct PdfDocument(Arc<DocumentRepr>);
impl PdfDocument {
/// Load a PDF document.
#[comemo::memoize]
#[typst_macros::time(name = "load pdf document")]
pub fn new(data: Bytes, world: Tracked<dyn World + '_>) -> LoadResult<PdfDocument> {
// 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<H: Hasher>(&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<ImageRepr>);
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<PdfImage> {
// 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<Pdf> {
&self.0.document.0.pdf
}
pub fn width(&self) -> f32 {
self.0.width
}
pub fn standard_fonts(&self) -> &Arc<StandardFonts> {
&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<dyn World + '_>) -> Arc<StandardFonts> {
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<Bytes>,
pub bold: Option<Bytes>,
pub italic: Option<Bytes>,
pub bold_italic: Option<Bytes>,
}
pub struct StandardFonts {
pub helvetica: VariantFont,
pub courier: VariantFont,
pub times: VariantFont,
pub symbol: Option<Bytes>,
pub zapf_dingbats: Option<Bytes>,
}

View File

@ -9,6 +9,7 @@ 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_svg::render_svg_glyph;
@ -363,6 +364,39 @@ fn finish(
hint: "convert the image to 8 bit instead"
)
}
KrillaError::Pdf(_, e, loc) => {
let span = to_span(loc);
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);
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 +569,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 +610,13 @@ fn convert_error(
"{prefix} missing document date";
hint: "set the date of the document"
),
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"
)
}
}
}

View File

@ -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<Repr>);
struct PdfRasterImage(Arc<Repr>);
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<H: Hasher>(&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 {

View File

@ -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()),
)),

View File

@ -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 }

View File

@ -1,11 +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::visualize::{Image, ImageKind, ImageScaling};
use typst_library::visualize::{Image, ImageKind, ImageScaling, PdfImage};
use crate::{AbsExt, State};
@ -59,9 +60,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<Arc<sk::Pixmap>> {
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 +86,72 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
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) => build_pdf_texture(pdf, w, h)?,
};
Some(Arc::new(texture))
}
// Keep this in sync with `typst-svg`!
fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option<sk::Pixmap> {
let sf = pdf.standard_fonts().clone();
let select_standard_font = move |font: StandardFont| -> Option<FontData> {
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<dyn AsRef<[u8]> + 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)?)
}

View File

@ -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 }

View File

@ -1,10 +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 typst_library::foundations::Smart;
use typst_library::layout::{Abs, Axes};
use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat,
};
use crate::SVGRenderer;
@ -44,7 +48,7 @@ pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> 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 +57,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,10 +66,29 @@ 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::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 only scale up as much so that the
// largest dimension doesn't exceed 3000 pixels.
const MIN_RES: f32 = 1000.0;
const MAX_RES: f32 = 3000.0;
let base_width = pdf.width();
let w_scale = (MIN_RES / base_width).max(MAX_RES / base_width);
let base_height = pdf.height();
let h_scale = (MIN_RES / base_height).min(MAX_RES / 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,");
@ -73,3 +96,54 @@ 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<u8> {
let sf = pdf.standard_fonts().clone();
let select_standard_font = move |font: StandardFont| -> Option<FontData> {
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<dyn AsRef<[u8]> + 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()
}