From 81e848140fb07c33cadd42054f1c958031ee7cbe Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:23:01 +0100 Subject: [PATCH] Initial commit --- Cargo.lock | 200 ++++++++++++++++++-- Cargo.toml | 7 +- crates/typst-pdf/Cargo.toml | 1 + crates/typst-pdf/src/image.rs | 9 +- crates/typst-pdf/src/krilla.rs | 324 +++++++++++++++++++++++++++++++++ crates/typst-pdf/src/lib.rs | 2 + 6 files changed, 523 insertions(+), 20 deletions(-) create mode 100644 crates/typst-pdf/src/krilla.rs diff --git a/Cargo.lock b/Cargo.lock index bfd1ccda6..5688e66de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,20 @@ name = "bytemuck" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -742,12 +756,30 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "font-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -771,6 +803,20 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "fontdb" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -1255,6 +1301,34 @@ dependencies = [ "libc", ] +[[package]] +name = "krilla" +version = "0.3.0" +dependencies = [ + "base64", + "bumpalo", + "flate2", + "float-cmp 0.10.0", + "fontdb 0.22.0", + "gif", + "image-webp", + "miniz_oxide", + "once_cell", + "pdf-writer 0.12.0 (git+https://github.com/LaurenzV/pdf-writer?rev=f95a19c)", + "resvg 0.44.0", + "rustybuzz", + "siphasher 1.0.1", + "skrifa", + "subsetter", + "tiny-skia", + "tiny-skia-path", + "usvg 0.44.0", + "xmp-writer 0.3.0 (git+https://github.com/LaurenzV/xmp-writer?rev=1c2b8ae9)", + "yoke", + "zune-jpeg", + "zune-png", +] + [[package]] name = "kurbo" version = "0.11.1" @@ -1694,6 +1768,17 @@ dependencies = [ "ryu", ] +[[package]] +name = "pdf-writer" +version = "0.12.0" +source = "git+https://github.com/LaurenzV/pdf-writer?rev=f95a19c#f95a19c07a1b3e3ee021c1199e91f19badb57d46" +dependencies = [ + "bitflags 2.6.0", + "itoa", + "memchr", + "ryu", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1943,6 +2028,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -2005,7 +2100,24 @@ dependencies = [ "rgb", "svgtypes", "tiny-skia", - "usvg", + "usvg 0.43.0", + "zune-jpeg", +] + +[[package]] +name = "resvg" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg 0.44.0", "zune-jpeg", ] @@ -2267,6 +2379,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -2313,7 +2435,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp", + "float-cmp 0.9.0", ] [[package]] @@ -2367,18 +2489,18 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5014c9dadcf318fb7ef8c16438e95abcc9de1ae24d60d5bccc64c55100c50364" dependencies = [ - "fontdb", + "fontdb 0.21.0", "image", "log", "miniz_oxide", "once_cell", - "pdf-writer", - "resvg", + "pdf-writer 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "resvg 0.43.0", "siphasher 1.0.1", "subsetter", "tiny-skia", "ttf-parser", - "usvg", + "usvg 0.43.0", ] [[package]] @@ -2797,7 +2919,7 @@ dependencies = [ "ecow", "env_proxy", "flate2", - "fontdb", + "fontdb 0.22.0", "native-tls", "once_cell", "openssl", @@ -2853,7 +2975,7 @@ dependencies = [ "csv", "ecow", "flate2", - "fontdb", + "fontdb 0.22.0", "hayagriva", "icu_properties", "icu_provider", @@ -2892,7 +3014,7 @@ dependencies = [ "unicode-math-class", "unicode-segmentation", "unscanny", - "usvg", + "usvg 0.44.0", "wasmi", "xmlwriter", ] @@ -2918,8 +3040,9 @@ dependencies = [ "ecow", "image", "indexmap 2.6.0", + "krilla", "miniz_oxide", - "pdf-writer", + "pdf-writer 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "subsetter", "svg2pdf", @@ -2930,7 +3053,7 @@ dependencies = [ "typst-syntax", "typst-timing", "typst-utils", - "xmp-writer", + "xmp-writer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2957,7 +3080,7 @@ dependencies = [ "comemo", "image", "pixglyph", - "resvg", + "resvg 0.44.0", "tiny-skia", "ttf-parser", "typst-library", @@ -3188,7 +3311,34 @@ dependencies = [ "base64", "data-url", "flate2", - "fontdb", + "fontdb 0.21.0", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher 1.0.1", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb 0.22.0", "imagesize", "kurbo", "log", @@ -3579,6 +3729,11 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8254499146a4fd0c86e3e99cf4a9f468f595808fb49ff8f3e495f2b117bf4ebc" +[[package]] +name = "xmp-writer" +version = "0.3.0" +source = "git+https://github.com/LaurenzV/xmp-writer?rev=1c2b8ae9#1c2b8ae9c217ceeec39b86cf5e215b67fe8870db" + [[package]] name = "xz2" version = "0.1.7" @@ -3745,6 +3900,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zune-jpeg" version = "0.4.13" @@ -3753,3 +3917,13 @@ checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" dependencies = [ "zune-core", ] + +[[package]] +name = "zune-png" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29c085769c6f29effea890f093120ac019375fdc789d2a496ba8ba96c77509" +dependencies = [ + "zune-core", + "zune-inflate", +] diff --git a/Cargo.toml b/Cargo.toml index e1f6dccb4..e91641ac0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ dirs = "5" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" flate2 = "1" -fontdb = { version = "0.21", default-features = false } +fontdb = { version = "0.22", default-features = false } fs_extra = "1.3" hayagriva = "0.8" heck = "0.5" @@ -68,6 +68,7 @@ if_chain = "1" image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.5" +krilla = { path = "../krilla/crates/krilla" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" @@ -92,7 +93,7 @@ qcms = "0.3.0" quote = "1" rayon = "1.7.0" regex = "1" -resvg = { version = "0.43", default-features = false, features = ["raster-images"] } +resvg = { version = "0.44", default-features = false, features = ["raster-images"] } roxmltree = "0.20" rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] } rustybuzz = "0.18" @@ -126,7 +127,7 @@ unicode-script = "0.5" unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } -usvg = { version = "0.43", default-features = false, features = ["text"] } +usvg = { version = "0.44", default-features = false, features = ["text"] } walkdir = "2" wasmi = "0.39.0" xmlparser = "0.13.5" diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index bc0da06c3..912edfaa9 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -26,6 +26,7 @@ comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } indexmap = { workspace = true } +krilla = { workspace = true } miniz_oxide = { workspace = true } pdf-writer = { workspace = true } serde = { workspace = true } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 9651d31ba..85439164e 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -206,10 +206,11 @@ fn encode_svg( svg: &SvgImage, pdfa: bool, ) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { - svg2pdf::to_chunk( - svg.tree(), - svg2pdf::ConversionOptions { pdfa, ..Default::default() }, - ) + unimplemented!(); + // svg2pdf::to_chunk( + // svg.tree(), + // svg2pdf::ConversionOptions { pdfa, ..Default::default() }, + // ) } /// A pre-encoded image. diff --git a/crates/typst-pdf/src/krilla.rs b/crates/typst-pdf/src/krilla.rs new file mode 100644 index 000000000..259228859 --- /dev/null +++ b/crates/typst-pdf/src/krilla.rs @@ -0,0 +1,324 @@ + +use crate::{AbsExt}; +use krilla::color::rgb; +use krilla::font::{GlyphUnits}; +use krilla::geom::{Point, Transform}; +use krilla::path::{Fill, PathBuilder, Stroke}; +use krilla::surface::Surface; +use krilla::{PageSettings, SerializeSettings, SvgSettings}; +use std::collections::HashMap; +use std::ops::Range; +use std::sync::Arc; +use image::{GenericImageView, Rgba}; +use krilla::validation::Validator; +use krilla::version::PdfVersion; +use svg2pdf::usvg::{NormalizedF32, Rect}; +use typst_library::layout::{Frame, FrameItem, GroupItem, Size}; +use typst_library::model::Document; +use typst_library::text::{Font, Glyph, TextItem}; +use typst_library::visualize::{ColorSpace, FillRule, FixedStroke, Geometry, Image, ImageKind, LineCap, LineJoin, Paint, Path, PathItem, Shape}; + +pub struct ExportContext { + fonts: HashMap, +} + +impl ExportContext { + pub fn new() -> Self { + Self { + fonts: Default::default(), + } + } +} + +// TODO: Change rustybuzz cluster behavior so it works with ActualText + +#[typst_macros::time(name = "write pdf")] +pub fn pdf(typst_document: &Document) -> Vec { + let settings = SerializeSettings { + compress_content_streams: true, + no_device_cs: false, + ascii_compatible: false, + xmp_metadata: true, + force_type3_fonts: false, + cmyk_profile: None, + validator: Validator::None, + enable_tagging: false, + pdf_version: PdfVersion::Pdf17, + }; + + let mut document = krilla::Document::new_with(settings); + let mut context = ExportContext::new(); + + for typst_page in &typst_document.pages { + let settings = PageSettings::new( + typst_page.frame.width().to_f32(), + typst_page.frame.height().to_f32(), + ); + let mut page = document.start_page_with(settings); + let mut surface = page.surface(); + process_frame(&typst_page.frame, &mut surface, &mut context); + } + + finish(document) +} + +#[typst_macros::time(name = "finish document")] +pub fn finish(document: krilla::Document) -> Vec { + // TODO: Don't unwrap + document.finish().unwrap() +} + +pub fn handle_group( + group: &GroupItem, + surface: &mut Surface, + context: &mut ExportContext, +) { + surface.push_transform(&convert_transform(group.transform)); + process_frame(&group.frame, surface, context); + surface.pop(); +} + +pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportContext) { + let font = context + .fonts + .entry(t.font.clone()) + .or_insert_with(|| { + krilla::font::Font::new( + // TODO: Don't do to_vec here! + Arc::new(t.font.data().to_vec()), + t.font.index(), + vec![], + ) + // TODO: DOn't unwrap + .unwrap() + }) + .clone(); + let (paint, opacity) = convert_paint(&t.fill); + let fill = Fill { + paint, + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + ..Default::default() + }; + let text = t.text.as_str(); + let size = t.size; + + // TODO: Avoid creating vector? + let glyphs = t.glyphs.iter().map(|g| WrapperGlyph(g.clone())).collect::>(); + + surface.fill_glyphs( + Point::from_xy(0.0, 0.0), + fill, + &glyphs, + font.clone(), + text, + size.to_f32(), + GlyphUnits::Normalized, + false, + ); + + if let Some(stroke) = t.stroke.as_ref().map(convert_fixed_stroke) { + surface.stroke_glyphs( + Point::from_xy(0.0, 0.0), + stroke, + &glyphs, + font.clone(), + text, + size.to_f32(), + GlyphUnits::Normalized, + true, + ); + } +} + +#[typst_macros::time(name = "handle image")] +pub fn handle_image( + image: &Image, + size: &Size, + surface: &mut Surface, + _: &mut ExportContext, +) { + match image.kind() { + ImageKind::Raster(raster) => { + let image = krilla::image::Image::from_png(raster.data()) + .unwrap(); + surface.draw_image( + image, + krilla::geom::Size::from_wh(size.x.to_f32(), size.y.to_f32()).unwrap(), + ); + } + ImageKind::Svg(svg) => { + surface.draw_svg( + svg.tree(), + krilla::geom::Size::from_wh(size.x.to_f32(), size.y.to_f32()).unwrap(), + SvgSettings::default(), + ); + } + } +} + +pub fn handle_shape(shape: &Shape, surface: &mut Surface) { + let mut path_builder = PathBuilder::new(); + + match &shape.geometry { + Geometry::Line(l) => { + path_builder.move_to(0.0, 0.0); + path_builder.line_to(l.x.to_f32(), l.y.to_f32()); + } + Geometry::Rect(r) => { + path_builder.push_rect( + Rect::from_xywh(0.0, 0.0, r.x.to_f32(), r.y.to_f32()).unwrap(), + ); + } + Geometry::Path(p) => { + convert_path(p, &mut path_builder); + } + } + + if let Some(path) = path_builder.finish() { + if let Some(paint) = &shape.fill { + let (paint, opacity) = convert_paint(paint); + + let fill = Fill { + paint, + rule: convert_fill_rule(shape.fill_rule), + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + }; + surface.fill_path(&path, fill); + } + + if let Some(stroke) = &shape.stroke { + let stroke = convert_fixed_stroke(stroke); + + surface.stroke_path(&path, stroke); + } + } +} + +pub fn convert_path(path: &Path, builder: &mut PathBuilder) { + for item in &path.0 { + match item { + PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()), + PathItem::LineTo(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()), + PathItem::CubicTo(p1, p2, p3) => builder.cubic_to( + p1.x.to_f32(), + p1.y.to_f32(), + p2.x.to_f32(), + p2.y.to_f32(), + p3.x.to_f32(), + p3.y.to_f32(), + ), + PathItem::ClosePath => builder.close(), + } + } +} + +pub fn process_frame(frame: &Frame, surface: &mut Surface, context: &mut ExportContext) { + for (point, item) in frame.items() { + surface.push_transform(&Transform::from_translate( + point.x.to_f32(), + point.y.to_f32(), + )); + + match item { + FrameItem::Group(g) => handle_group(g, surface, context), + FrameItem::Text(t) => handle_text(t, surface, context), + FrameItem::Shape(s, _) => handle_shape(s, surface), + FrameItem::Image(image, size, _) => { + handle_image(image, size, surface, context) + } + FrameItem::Link(_, _) => {} + FrameItem::Tag(_) => {} + } + + surface.pop(); + } +} + +struct WrapperGlyph(Glyph); + +impl krilla::font::Glyph for WrapperGlyph { + fn glyph_id(&self) -> krilla::font::GlyphId { + krilla::font::GlyphId::new(self.0.id as u32) + } + + fn text_range(&self) -> Range { + self.0.range.start as usize..self.0.range.end as usize + } + + fn x_advance(&self) -> f32 { + self.0.x_advance.get() as f32 + } + + fn x_offset(&self) -> f32 { + self.0.x_offset.get() as f32 + } + + fn y_offset(&self) -> f32 { + 0.0 + } + + fn y_advance(&self) -> f32 { + 0.0 + } +} + +fn convert_fill_rule(fill_rule: FillRule) -> krilla::path::FillRule { + match fill_rule { + FillRule::NonZero => krilla::path::FillRule::NonZero, + FillRule::EvenOdd => krilla::path::FillRule::EvenOdd, + } +} + +fn convert_fixed_stroke(stroke: &FixedStroke) -> Stroke { + let (paint, opacity) = convert_paint(&stroke.paint); + Stroke { + paint, + width: stroke.thickness.to_f32(), + miter_limit: stroke.miter_limit.get() as f32, + line_join: convert_linejoin(stroke.join), + line_cap: convert_linecap(stroke.cap), + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + ..Default::default() + } +} + +fn convert_linecap(l: LineCap) -> krilla::path::LineCap { + match l { + LineCap::Butt => krilla::path::LineCap::Butt, + LineCap::Round => krilla::path::LineCap::Round, + LineCap::Square => krilla::path::LineCap::Square, + } +} + +fn convert_linejoin(l: LineJoin) -> krilla::path::LineJoin { + match l { + LineJoin::Miter => krilla::path::LineJoin::Miter, + LineJoin::Round => krilla::path::LineJoin::Round, + LineJoin::Bevel => krilla::path::LineJoin::Bevel, + } +} + +fn convert_transform(t: crate::Transform) -> krilla::geom::Transform { + Transform::from_row( + t.sx.get() as f32, + t.ky.get() as f32, + t.kx.get() as f32, + t.sy.get() as f32, + t.tx.to_f32(), + t.ty.to_f32(), + ) +} + +fn convert_paint(paint: &Paint) -> (krilla::paint::Paint, u8) { + match paint { + Paint::Solid(c) => { + let components = c.to_space(ColorSpace::Srgb).to_vec4_u8(); + ( + rgb::Color::new(components[0], components[1], components[2]).into(), + components[3], + ) + } + Paint::Gradient(_) => (rgb::Color::black().into(), 255), + Paint::Pattern(_) => (rgb::Color::black().into(), 255), + } +} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index efc99b749..4c0d52c14 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -8,6 +8,7 @@ mod extg; mod font; mod gradient; mod image; +mod krilla; mod named_destination; mod outline; mod page; @@ -50,6 +51,7 @@ use crate::resources::{ /// Returns the raw bytes making up the PDF file. #[typst_macros::time(name = "pdf")] pub fn pdf(document: &Document, options: &PdfOptions) -> SourceResult> { + return Ok(krilla::pdf(document)); PdfBuilder::new(document, options) .phase(|builder| builder.run(traverse_pages))? .phase(|builder| {