Initial commit

This commit is contained in:
Laurenz Stampfl 2024-11-14 23:23:01 +01:00
parent 7add9b459a
commit 81e848140f
6 changed files with 523 additions and 20 deletions

200
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@ -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<Font, krilla::font::Font>,
}
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<u8> {
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<u8> {
// 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::<Vec<_>>();
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<usize> {
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),
}
}

View File

@ -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<Vec<u8>> {
return Ok(krilla::pdf(document));
PdfBuilder::new(document, options)
.phase(|builder| builder.run(traverse_pages))?
.phase(|builder| {