From 8f7ba8d4958184366180efeb00ecfe8835c69b11 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Jun 2024 17:30:49 +0200 Subject: [PATCH] Bump SVG & PDF cinematic universe (#4316) --- Cargo.lock | 188 +++++----- Cargo.toml | 20 +- crates/typst-pdf/src/image.rs | 32 +- crates/typst-pdf/src/page.rs | 49 +-- crates/typst-render/src/image.rs | 17 +- crates/typst/src/layout/inline/shaping.rs | 3 +- crates/typst/src/text/font/color.rs | 75 ++-- crates/typst/src/text/font/variant.rs | 22 ++ crates/typst/src/text/mod.rs | 4 +- crates/typst/src/visualize/image/svg.rs | 402 +++++++++++----------- 10 files changed, 404 insertions(+), 408 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ce3e15fd..99295d902 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,7 +392,7 @@ dependencies = [ "comemo-macros", "once_cell", "parking_lot", - "siphasher 1.0.0", + "siphasher 1.0.1", ] [[package]] @@ -701,14 +701,14 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" dependencies = [ - "roxmltree", + "roxmltree 0.19.0", ] [[package]] name = "fontdb" -version = "0.16.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" dependencies = [ "fontconfig-parser", "log", @@ -783,16 +783,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "gif" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gif" version = "0.13.1" @@ -1054,12 +1044,28 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", - "gif 0.13.1", + "gif", "jpeg-decoder", "num-traits", "png", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + [[package]] name = "imagesize" version = "0.12.0" @@ -1203,11 +1209,12 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.9.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" dependencies = [ "arrayvec", + "smallvec", ] [[package]] @@ -1612,11 +1619,11 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pdf-writer" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e9127455063c816e661caac9ecd9043ad2871f55be93014e6838a8ced2332b" +checksum = "af6a7882fda7808481d43c51cadfc3ec934c6af72612a1fe6985ce329a2f0469" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "itoa", "memchr", "ryu", @@ -1678,9 +1685,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pixglyph" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e0f8ad4c197db38125b880c3c44544788665c7d5f4c42f5a35da44bca1a712" +checksum = "4a64dec9fae2e0f75bb2a7d910b4e8b0d15ecd706fb0d61394774b3223c50a97" dependencies = [ "ttf-parser", ] @@ -1904,15 +1911,14 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "resvg" -version = "0.38.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34501046959e06470ba62a2dc7f31c15f94ac250d842a45f9e012f4ee40c1e" +checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" dependencies = [ - "gif 0.12.0", + "gif", "jpeg-decoder", "log", "pico-args", - "png", "rgb", "svgtypes", "tiny-skia", @@ -1940,6 +1946,12 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1976,9 +1988,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rustybuzz" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" +checksum = "7730060ad401b0d1807c904ea56735288af101430aa0d2ab8358b789f5f37002" dependencies = [ "bitflags 2.4.2", "bytemuck", @@ -2160,9 +2172,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ac45299ccbd390721be55b412d41931911f654fa99e2cb8bfb57184b2061fe" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slotmap" @@ -2249,27 +2261,33 @@ checksum = "09eab8a83bff89ba2200bd4c59be45c7c787f988431b936099a5a266c957f2f9" [[package]] name = "svg2pdf" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba36b330062be8497fd96597227a757b621b86c4d24d164b06e4522b52b3693e" +checksum = "e31565956eb1dc398c0d9776ee1d1bac4e34759af63dcbe0520df32313a5b53b" dependencies = [ - "image", + "fontdb", + "image 0.25.1", + "log", "miniz_oxide", "once_cell", "pdf-writer", "resvg", + "siphasher 1.0.1", + "subsetter", "tiny-skia", + "ttf-parser", + "unicode-properties", "usvg", ] [[package]] name = "svgtypes" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" dependencies = [ "kurbo", - "siphasher 0.3.11", + "siphasher 1.0.1", ] [[package]] @@ -2499,9 +2517,9 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "two-face" @@ -2541,7 +2559,7 @@ dependencies = [ "icu_provider_blob", "icu_segmenter", "if_chain", - "image", + "image 0.24.9", "indexmap 2.2.5", "kamadak-exif", "kurbo", @@ -2555,12 +2573,12 @@ dependencies = [ "qcms", "rayon", "regex", - "roxmltree", + "roxmltree 0.20.0", "rustybuzz", "serde", "serde_json", "serde_yaml 0.9.32", - "siphasher 1.0.0", + "siphasher 1.0.1", "smallvec", "stacker", "syntect", @@ -2710,7 +2728,7 @@ dependencies = [ "bytemuck", "comemo", "ecow", - "image", + "image 0.24.9", "indexmap 2.2.5", "miniz_oxide", "once_cell", @@ -2733,10 +2751,10 @@ version = "0.11.0" dependencies = [ "bytemuck", "comemo", - "image", + "image 0.24.9", "pixglyph", "resvg", - "roxmltree", + "roxmltree 0.20.0", "tiny-skia", "ttf-parser", "typst", @@ -2817,7 +2835,7 @@ dependencies = [ "once_cell", "portable-atomic", "rayon", - "siphasher 1.0.0", + "siphasher 1.0.1", "thin-vec", ] @@ -2857,15 +2875,15 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-bidi-mirroring" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" [[package]] name = "unicode-ccc" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" @@ -2960,62 +2978,29 @@ dependencies = [ [[package]] name = "usvg" -version = "0.38.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377f62b4a3c173de8654c1aa80ab1dac1154e6f13a779a9943e53780120d1625" -dependencies = [ - "base64 0.21.7", - "log", - "pico-args", - "usvg-parser", - "usvg-text-layout", - "usvg-tree", - "xmlwriter", -] - -[[package]] -name = "usvg-parser" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a05e6f2023d6b4e946f734240a3927aefdcf930d7d42587a2c8a8869814b0" +checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" dependencies = [ + "base64 0.22.0", "data-url", "flate2", + "fontdb", "imagesize", "kurbo", "log", - "roxmltree", - "simplecss", - "siphasher 0.3.11", - "svgtypes", - "usvg-tree", -] - -[[package]] -name = "usvg-text-layout" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c41888b9d5cf431fe852eaf9d047bbde83251b98f1749c2f08b1071e6db46e2" -dependencies = [ - "fontdb", - "kurbo", - "log", + "pico-args", + "roxmltree 0.20.0", "rustybuzz", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "usvg-tree", -] - -[[package]] -name = "usvg-tree" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18863e0404ed153d6e56362c5b1146db9f4f262a3244e3cf2dbe7d8a85909f05" -dependencies = [ + "simplecss", + "siphasher 1.0.1", "strict-num", "svgtypes", "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", ] [[package]] @@ -3514,3 +3499,18 @@ dependencies = [ "simd-adler32", "typed-arena", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 45e13bbfc..40051a5a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ dirs = "5" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" flate2 = "1" -fontdb = { version = "0.16", default-features = false } +fontdb = { version = "0.18", default-features = false } fs_extra = "1.3" hayagriva = "0.5.3" heck = "0.4" @@ -60,7 +60,7 @@ if_chain = "1" image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.5" -kurbo = "0.9" # in sync with usvg +kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" log = "0.4" @@ -74,9 +74,9 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" pathdiff = "0.2" -pdf-writer = "0.9.3" +pdf-writer = "0.10.0" phf = { version = "0.11", features = ["macros"] } -pixglyph = "0.3" +pixglyph = "0.4" png = "0.17" portable-atomic = "1.6" proc-macro2 = "1" @@ -85,9 +85,9 @@ quote = "1" qcms = "0.3.0" rayon = "1.7.0" regex = "1" -resvg = { version = "0.38.0", default-features = false, features = ["raster-images"] } -roxmltree = "0.19" -rustybuzz = "0.12.1" +resvg = { version = "0.42", default-features = false, features = ["raster-images"] } +roxmltree = "0.20" +rustybuzz = "0.14" same-file = "1" self-replace = "1.3.7" semver = "1" @@ -99,7 +99,7 @@ siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } stacker = "0.1.15" subsetter = "0.1.1" -svg2pdf = "0.10" +svg2pdf = "0.11.0" syn = { version = "2", features = ["full", "extra-traits"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } tar = "0.4" @@ -108,7 +108,7 @@ thin-vec = "0.2.13" time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] } tiny-skia = "0.11" toml = { version = "0.8", default-features = false, features = ["parse", "display"] } -ttf-parser = "0.20.0" +ttf-parser = "0.21.0" two-face = { version = "0.3.0", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" unicode-bidi = "0.3.13" @@ -119,7 +119,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.38.0", default-features = false, features = ["text"] } +usvg = { version = "0.42", default-features = false, features = ["text"] } walkdir = "2" wasmi = "0.31.0" xmlparser = "0.13.5" diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 9da7158ca..9951dac59 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -90,12 +90,12 @@ pub fn write_images(context: &WithGlobalRefs) -> (PdfChunk, HashMap) } } } - EncodedImage::Svg(svg_chunk) => { + EncodedImage::Svg(svg_chunk, id) => { let mut map = HashMap::new(); svg_chunk.renumber_into(&mut chunk.chunk, |old| { *map.entry(old).or_insert_with(|| chunk.alloc.bump()) }); - out.insert(image.clone(), map[&Ref::new(1)]); + out.insert(image.clone(), map[&id]); } } } @@ -132,7 +132,10 @@ pub fn deferred_image(image: Image) -> (Deferred, Option EncodedImage::Svg(encode_svg(svg)), + ImageKind::Svg(svg) => { + let (chunk, id) = encode_svg(svg); + EncodedImage::Svg(chunk, id) + } }); (deferred, color_space) @@ -176,25 +179,8 @@ fn encode_alpha(raster: &RasterImage) -> (Vec, Filter) { } /// Encode an SVG into a chunk of PDF objects. -/// -/// The main XObject will have ID 1. -fn encode_svg(svg: &SvgImage) -> Chunk { - let mut chunk = Chunk::new(); - - // Safety: We do not keep any references to tree nodes beyond the - // scope of `with`. - unsafe { - svg.with(|tree| { - svg2pdf::convert_tree_into( - tree, - svg2pdf::Options::default(), - &mut chunk, - Ref::new(1), - ); - }); - } - - chunk +fn encode_svg(svg: &SvgImage) -> (Chunk, Ref) { + svg2pdf::to_chunk(svg.tree(), svg2pdf::ConversionOptions::default()) } /// A pre-encoded image. @@ -219,5 +205,5 @@ pub enum EncodedImage { /// A vector graphic. /// /// The chunk is the SVG converted to PDF objects. - Svg(Chunk), + Svg(Chunk, Ref), } diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index f796d0c8f..c6881eb61 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -115,29 +115,12 @@ fn write_page( return; }; - let global_resources_ref = ctx.resources.reference; - let mut page_writer = chunk.page(page_ref); - page_writer.parent(page_tree_ref); - - let w = page.content.size.x.to_f32(); - let h = page.content.size.y.to_f32(); - page_writer.media_box(Rect::new(0.0, 0.0, w, h)); - page_writer.contents(content_id); - page_writer.pair(Name(b"Resources"), global_resources_ref); - - if page.content.uses_opacities { - page_writer - .group() - .transparency() - .isolated(false) - .knockout(false) - .color_space() - .srgb(); - } - - let mut annotations = page_writer.annotations(); + let mut annotations = Vec::with_capacity(page.content.links.len()); for (dest, rect) in &page.content.links { - let mut annotation = annotations.push(); + let id = chunk.alloc(); + annotations.push(id); + + let mut annotation = chunk.annotation(id); annotation.subtype(AnnotationType::Link).rect(*rect); annotation.border(0.0, 0.0, 0.0, None).flags(AnnotationFlags::PRINT); @@ -180,7 +163,27 @@ fn write_page( } } - annotations.finish(); + let mut page_writer = chunk.page(page_ref); + page_writer.parent(page_tree_ref); + + let w = page.content.size.x.to_f32(); + let h = page.content.size.y.to_f32(); + page_writer.media_box(Rect::new(0.0, 0.0, w, h)); + page_writer.contents(content_id); + page_writer.pair(Name(b"Resources"), ctx.resources.reference); + + if page.content.uses_opacities { + page_writer + .group() + .transparency() + .isolated(false) + .knockout(false) + .color_space() + .srgb(); + } + + page_writer.annotations(annotations); + page_writer.finish(); chunk diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 81b219de4..dcbf79827 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -72,15 +72,14 @@ fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { } // Safety: We do not keep any references to tree nodes beyond the scope // of `with`. - ImageKind::Svg(svg) => unsafe { - svg.with(|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 pixmap.as_mut()) - }); - }, + ImageKind::Svg(svg) => { + 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 pixmap.as_mut()) + } } Some(Arc::new(pixmap)) } diff --git a/crates/typst/src/layout/inline/shaping.rs b/crates/typst/src/layout/inline/shaping.rs index 57b94230c..15752f1ba 100644 --- a/crates/typst/src/layout/inline/shaping.rs +++ b/crates/typst/src/layout/inline/shaping.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use az::SaturatingAs; use ecow::EcoString; -use rustybuzz::{ShapePlan, Tag, UnicodeBuffer}; +use rustybuzz::{ShapePlan, UnicodeBuffer}; +use ttf_parser::Tag; use unicode_script::{Script, UnicodeScript}; use super::SpanMapper; diff --git a/crates/typst/src/text/font/color.rs b/crates/typst/src/text/font/color.rs index 8ca9e6c5b..ceddeeae7 100644 --- a/crates/typst/src/text/font/color.rs +++ b/crates/typst/src/text/font/color.rs @@ -4,12 +4,11 @@ use std::io::Read; use ecow::EcoString; use ttf_parser::GlyphId; -use usvg::{TreeParsing, TreeWriting}; use crate::layout::{Abs, Axes, Em, Frame, FrameItem, Point, Size}; use crate::syntax::Span; use crate::text::{Font, Glyph, Lang, TextItem}; -use crate::visualize::{Color, Image, Paint, Rgb}; +use crate::visualize::{Color, Image, Rgb}; /// Tells if a glyph is a color glyph or not in a given font. pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool { @@ -78,7 +77,8 @@ fn draw_raster_glyph( /// Draws a COLR glyph in a frame. fn draw_colr_glyph(frame: &mut Frame, font: &Font, glyph_id: GlyphId) { let mut painter = ColrPainter { font, current_glyph: glyph_id, frame }; - font.ttf().paint_color_glyph(glyph_id, 0, &mut painter); + let black = ttf_parser::RgbaColor::new(0, 0, 0, 255); + font.ttf().paint_color_glyph(glyph_id, 0, black, &mut painter); } /// Draws COLR glyphs in a frame. @@ -91,8 +91,20 @@ struct ColrPainter<'f, 't> { current_glyph: GlyphId, } -impl<'f, 't> ColrPainter<'f, 't> { - fn paint(&mut self, fill: Paint) { +impl<'f, 't> ttf_parser::colr::Painter<'_> for ColrPainter<'f, 't> { + fn outline_glyph(&mut self, glyph_id: GlyphId) { + self.current_glyph = glyph_id; + } + + fn paint(&mut self, paint: ttf_parser::colr::Paint) { + let ttf_parser::colr::Paint::Solid(color) = paint else { return }; + let color = Color::Rgb(Rgb::new( + color.red as f32 / 255.0, + color.green as f32 / 255.0, + color.blue as f32 / 255.0, + color.alpha as f32 / 255.0, + )); + self.frame.push( // With images, the position corresponds to the top-left corner, but // in the case of text it matches the baseline-left point. Here, we @@ -101,7 +113,7 @@ impl<'f, 't> ColrPainter<'f, 't> { FrameItem::Text(TextItem { font: self.font.clone(), size: Abs::pt(self.font.units_per_em()), - fill, + fill: color.into(), stroke: None, lang: Lang::ENGLISH, region: None, @@ -116,29 +128,21 @@ impl<'f, 't> ColrPainter<'f, 't> { span: (Span::detached(), 0), }], }), - ) - } -} - -impl<'f, 't> ttf_parser::colr::Painter for ColrPainter<'f, 't> { - fn outline(&mut self, glyph_id: GlyphId) { - self.current_glyph = glyph_id; + ); } - fn paint_foreground(&mut self) { - // Default to black if no color was specified - self.paint(Paint::Solid(Color::BLACK)) - } - - fn paint_color(&mut self, color: ttf_parser::RgbaColor) { - let color = Color::Rgb(Rgb::new( - color.red as f32 / 255.0, - color.green as f32 / 255.0, - color.blue as f32 / 255.0, - color.alpha as f32 / 255.0, - )); - self.paint(Paint::Solid(color)); - } + // These are not implemented. + fn push_clip(&mut self) {} + fn push_clip_box(&mut self, _: ttf_parser::colr::ClipBox) {} + fn pop_clip(&mut self) {} + fn push_layer(&mut self, _: ttf_parser::colr::CompositeMode) {} + fn pop_layer(&mut self) {} + fn push_translate(&mut self, _: f32, _: f32) {} + fn push_scale(&mut self, _: f32, _: f32) {} + fn push_rotate(&mut self, _: f32) {} + fn push_skew(&mut self, _: f32, _: f32) {} + fn push_transform(&mut self, _: ttf_parser::Transform) {} + fn pop_transform(&mut self) {} } /// Draws an SVG glyph in a frame. @@ -164,25 +168,16 @@ fn draw_svg_glyph( // Parse SVG. let opts = usvg::Options::default(); - let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; - - // Compute the space we need to draw our glyph. - // See https://github.com/RazrFalcon/resvg/issues/602 for why - // using the svg size is problematic here. - tree.calculate_bounding_boxes(); - let mut bbox = usvg::BBox::default(); - if let Some(tree_bbox) = tree.root.bounding_box { - bbox = bbox.expand(tree_bbox); - } - let bbox = bbox.to_rect()?; - - let mut data = tree.to_string(&usvg::XmlOptions::default()); + let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; + let bbox = tree.root().bounding_box(); let width = bbox.width() as f64; let height = bbox.height() as f64; let left = bbox.left() as f64; let top = bbox.top() as f64; + let mut data = tree.to_string(&usvg::WriteOptions::default()); + // The SVG coordinates and the font coordinates are not the same: the Y axis // is mirrored. But the origin of the axes are the same (which means that // the horizontal axis in the SVG document corresponds to the baseline). See diff --git a/crates/typst/src/text/font/variant.rs b/crates/typst/src/text/font/variant.rs index e34d17b6b..644d9f75d 100644 --- a/crates/typst/src/text/font/variant.rs +++ b/crates/typst/src/text/font/variant.rs @@ -141,6 +141,12 @@ impl Debug for FontWeight { } } +impl From for FontWeight { + fn from(weight: fontdb::Weight) -> Self { + Self::from_number(weight.0) + } +} + cast! { FontWeight, self => IntoValue::into_value(match self { @@ -237,6 +243,21 @@ impl FontStretch { Ratio::new(self.0 as f64 / 1000.0) } + /// Round to one of the pre-defined variants. + pub fn round(self) -> Self { + match self.0 { + ..=562 => Self::ULTRA_CONDENSED, + 563..=687 => Self::EXTRA_CONDENSED, + 688..=812 => Self::CONDENSED, + 813..=937 => Self::SEMI_CONDENSED, + 938..=1062 => Self::NORMAL, + 1063..=1187 => Self::SEMI_EXPANDED, + 1188..=1374 => Self::EXPANDED, + 1375..=1749 => Self::EXTRA_EXPANDED, + 1750.. => Self::ULTRA_EXPANDED, + } + } + /// The absolute ratio distance between this and another font stretch. pub fn distance(self, other: Self) -> Ratio { (self.to_ratio() - other.to_ratio()).abs() @@ -248,6 +269,7 @@ impl Default for FontStretch { Self::NORMAL } } + impl Repr for FontStretch { fn repr(&self) -> EcoString { self.to_ratio().repr() diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index 4b10b807b..9316d4a85 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -31,9 +31,9 @@ pub use self::space::*; use std::fmt::{self, Debug, Formatter}; use ecow::{eco_format, EcoString}; -use rustybuzz::{Feature, Tag}; +use rustybuzz::Feature; use smallvec::SmallVec; -use ttf_parser::Rect; +use ttf_parser::{Rect, Tag}; use crate::diag::{bail, warning, HintedStrResult, SourceResult}; use crate::engine::Engine; diff --git a/crates/typst/src/visualize/image/svg.rs b/crates/typst/src/visualize/image/svg.rs index de53a8589..399d8d5ad 100644 --- a/crates/typst/src/visualize/image/svg.rs +++ b/crates/typst/src/visualize/image/svg.rs @@ -1,17 +1,17 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use comemo::Tracked; use ecow::EcoString; -use once_cell::sync::Lazy; -use siphasher::sip128::Hasher128; -use usvg::{ImageHrefResolver, Node, PostProcessingSteps, TreeParsing, TreePostProc}; +use siphasher::sip128::{Hasher128, SipHasher13}; use crate::diag::{format_xml_like_error, StrResult}; use crate::foundations::Bytes; use crate::layout::Axes; -use crate::text::{FontVariant, FontWeight}; +use crate::text::{ + Font, FontBook, FontFlags, FontStretch, FontStyle, FontVariant, FontWeight, +}; use crate::visualize::Image; use crate::World; @@ -24,23 +24,16 @@ struct Repr { data: Bytes, size: Axes, font_hash: u128, - tree: sync::SyncTree, + tree: usvg::Tree, } impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] pub fn new(data: Bytes) -> StrResult { - let mut tree = - usvg::Tree::from_data(&data, &OPTIONS).map_err(format_usvg_error)?; - tree.calculate_bounding_boxes(); - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash: 0, - // Safety: We just created the tree and hold the only reference. - tree: unsafe { sync::SyncTree::new(tree) }, - }))) + let tree = + usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) } /// Decode an SVG image with access to fonts. @@ -50,22 +43,25 @@ impl SvgImage { world: Tracked, families: &[String], ) -> StrResult { - let mut tree = - usvg::Tree::from_data(&data, &OPTIONS).map_err(format_usvg_error)?; - let mut font_hash = 0; - if tree.has_text_nodes() { - let (fontdb, hash) = load_svg_fonts(world, &mut tree, families); - tree.postprocess(PostProcessingSteps::default(), &fontdb); - font_hash = hash; - } - tree.calculate_bounding_boxes(); - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash, - // Safety: We just created the tree and hold the only reference. - tree: unsafe { sync::SyncTree::new(tree) }, - }))) + let book = world.book(); + let resolver = Mutex::new(FontResolver::new(world, book, families)); + let tree = usvg::Tree::from_data( + &data, + &usvg::Options { + font_resolver: usvg::FontResolver { + select_font: Box::new(|font, db| { + resolver.lock().unwrap().select_font(font, db) + }), + select_fallback: Box::new(|c, exclude_fonts, db| { + resolver.lock().unwrap().select_fallback(c, exclude_fonts, db) + }), + }, + ..base_options() + }, + ) + .map_err(format_usvg_error)?; + let font_hash = resolver.into_inner().unwrap().finish(); + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) } /// The raw image data. @@ -83,34 +79,9 @@ impl SvgImage { self.0.size.y } - /// Performs an operation with the usvg tree. - /// - /// This makes the tree uniquely available to the current thread and blocks - /// other accesses to it. - /// - /// # Safety - /// The caller may not hold any references to `Rc`s contained in the usvg - /// Tree after `f` returns. - /// - /// # Why is it unsafe? - /// Sadly, usvg's Tree is neither `Sync` nor `Send` because it uses `Rc` - /// internally and sending a tree to another thread could result in data - /// races when an `Rc`'s ref-count is modified from two threads at the same - /// time. - /// - /// However, access to the tree is actually safe if we don't clone `Rc`s / - /// only clone them while holding a mutex and drop all clones before the - /// mutex is released. Sadly, we can't enforce this variant at the type - /// system level. Therefore, access is guarded by this function (which makes - /// it reasonable hard to keep references around) and its usage still - /// remains `unsafe` (because it's still possible to have `Rc`s escape). - /// - /// See also: - pub unsafe fn with(&self, f: F) - where - F: FnOnce(&usvg::Tree), - { - self.0.tree.with(f) + /// Accesses the usvg tree. + pub fn tree(&self) -> &usvg::Tree { + &self.0.tree } } @@ -124,127 +95,31 @@ impl Hash for Repr { } } -/// The conversion options. -static OPTIONS: Lazy = Lazy::new(|| usvg::Options { - // Disable usvg's default to "Times New Roman". Instead, we default to - // the empty family and later, when we traverse the SVG, we check for - // empty and non-existing family names and replace them with the true - // fallback family. This way, we can memoize SVG decoding with and - // without fonts if the SVG does not contain text. - font_family: String::new(), +/// The base conversion options, to be extended with font-related options +/// because those can change across the document. +fn base_options() -> usvg::Options<'static> { + usvg::Options { + // Disable usvg's default to "Times New Roman". + font_family: String::new(), - // We override the DPI here so that we get the correct the size when - // scaling the image to its natural size. - dpi: Image::DEFAULT_DPI as f32, + // We override the DPI here so that we get the correct the size when + // scaling the image to its natural size. + dpi: Image::DEFAULT_DPI as f32, - // Override usvg's resource loading defaults. - resources_dir: None, - image_href_resolver: ImageHrefResolver { - resolve_data: ImageHrefResolver::default_data_resolver(), - resolve_string: Box::new(|_, _| None), - }, + // Override usvg's resource loading defaults. + resources_dir: None, + image_href_resolver: usvg::ImageHrefResolver { + resolve_data: usvg::ImageHrefResolver::default_data_resolver(), + resolve_string: Box::new(|_, _| None), + }, - ..Default::default() -}); - -/// Discover and load the fonts referenced by an SVG. -fn load_svg_fonts( - world: Tracked, - tree: &mut usvg::Tree, - families: &[String], -) -> (fontdb::Database, u128) { - let book = world.book(); - let mut fontdb = fontdb::Database::new(); - let mut hasher = siphasher::sip128::SipHasher13::new(); - let mut loaded = HashMap::>::new(); - - // Loads a font into the database and return it's usvg-compatible name. - let mut load_into_db = |id: usize| -> Option { - loaded - .entry(id) - .or_insert_with(|| { - let font = world.font(id)?; - fontdb.load_font_source(fontdb::Source::Binary(Arc::new( - font.data().clone(), - ))); - font.data().hash(&mut hasher); - font.find_name(ttf_parser::name_id::TYPOGRAPHIC_FAMILY) - .or_else(|| font.find_name(ttf_parser::name_id::FAMILY)) - }) - .clone() - }; - - // Determine the best font for each text node. - for child in &mut tree.root.children { - traverse_svg(child, &mut |node| { - let usvg::Node::Text(ref mut text) = node else { return }; - for chunk in &mut text.chunks { - 'spans: for span in &mut chunk.spans { - let Some(text) = chunk.text.get(span.start..span.end) else { - continue; - }; - let variant = FontVariant { - style: span.font.style.into(), - weight: FontWeight::from_number(span.font.weight), - stretch: span.font.stretch.into(), - }; - - // Find a font that covers the whole text among the span's fonts - // and the current document font families. - let mut like = None; - for family in span.font.families.iter().chain(families) { - let Some(id) = book.select(&family.to_lowercase(), variant) - else { - continue; - }; - let Some(info) = book.info(id) else { continue }; - like.get_or_insert(info); - - if text.chars().all(|c| info.coverage.contains(c as u32)) { - if let Some(usvg_family) = load_into_db(id) { - span.font.families = vec![usvg_family]; - continue 'spans; - } - } - } - - // If we didn't find a match, select a fallback font. - if let Some(id) = book.select_fallback(like, variant, text) { - if let Some(usvg_family) = load_into_db(id) { - span.font.families = vec![usvg_family]; - } - } - } - } - }); - } - - (fontdb, hasher.finish128().as_u128()) -} - -/// Search for all font families referenced by an SVG. -fn traverse_svg(node: &mut usvg::Node, f: &mut F) -where - F: FnMut(&mut usvg::Node), -{ - f(node); - - node.subroots_mut(|subroot| { - for child in &mut subroot.children { - traverse_svg(child, f); - } - }); - - if let Node::Group(ref mut group) = node { - for child in &mut group.children { - traverse_svg(child, f); - } + ..Default::default() } } -/// The ceiled pixel size of an SVG. +/// The pixel size of an SVG. fn tree_size(tree: &usvg::Tree) -> Axes { - Axes::new(tree.size.width() as f64, tree.size.height() as f64) + Axes::new(tree.size().width() as f64, tree.size().height() as f64) } /// Format the user-facing SVG decoding error message. @@ -260,40 +135,155 @@ fn format_usvg_error(error: usvg::Error) -> EcoString { } } -mod sync { - use std::sync::Mutex; +/// Provides Typst's fonts to usvg. +struct FontResolver<'a> { + /// Typst's font book. + book: &'a FontBook, + /// The world we use to load fonts. + world: Tracked<'a, dyn World + 'a>, + /// The active list of font families at the location of the SVG. + families: &'a [String], + /// A mapping from Typst font indices to fontdb IDs. + to_id: HashMap>, + /// The reverse mapping. + from_id: HashMap, + /// Accumulates a hash of all used fonts. + hasher: SipHasher13, +} - /// A synchronized wrapper around a `usvg::Tree`. - pub struct SyncTree(Mutex); - - impl SyncTree { - /// Create a new synchronized tree. - /// - /// # Safety - /// The tree must be completely owned by `tree`, there may not be any - /// other references to `Rc`s contained in it. - pub unsafe fn new(tree: usvg::Tree) -> Self { - Self(Mutex::new(tree)) - } - - /// Perform an operation with the usvg tree. - /// - /// # Safety - /// The caller may not hold any references to `Rc`s contained in - /// the usvg Tree after returning. - pub unsafe fn with(&self, f: F) - where - F: FnOnce(&usvg::Tree), - { - let tree = self.0.lock().unwrap(); - f(&tree) +impl<'a> FontResolver<'a> { + /// Create a new font provider. + fn new( + world: Tracked<'a, dyn World + 'a>, + book: &'a FontBook, + families: &'a [String], + ) -> Self { + Self { + book, + world, + families, + to_id: HashMap::new(), + from_id: HashMap::new(), + hasher: SipHasher13::new(), } } - // Safety: usvg's Tree is only non-Sync and non-Send because it uses `Rc` - // internally. By wrapping it in a mutex and forbidding outstanding - // references to the tree to remain after a `with` call, we guarantee that - // no two threads try to change a ref-count at the same time. - unsafe impl Sync for SyncTree {} - unsafe impl Send for SyncTree {} + /// Returns a hash of all used fonts. + fn finish(self) -> u128 { + self.hasher.finish128().as_u128() + } +} + +impl FontResolver<'_> { + /// Select a font. + fn select_font( + &mut self, + font: &usvg::Font, + db: &mut Arc, + ) -> Option { + let variant = FontVariant { + style: font.style().into(), + weight: FontWeight::from_number(font.weight()), + stretch: font.stretch().into(), + }; + + // Find a family that is available. + font.families() + .iter() + .filter_map(|family| match family { + usvg::FontFamily::Named(named) => Some(named), + // We don't support generic families at the moment. + _ => None, + }) + .chain(self.families) + .filter_map(|named| self.book.select(&named.to_lowercase(), variant)) + .find_map(|index| self.get_or_load(index, db)) + } + + /// Select a fallback font. + fn select_fallback( + &mut self, + c: char, + exclude_fonts: &[fontdb::ID], + db: &mut Arc, + ) -> Option { + // Get the font info of the originally selected font. + let like = exclude_fonts + .first() + .and_then(|first| self.from_id.get(first)) + .map(|font| font.info()); + + // usvg doesn't provide a variant in the fallback handler, but + // `exclude_fonts` is actually never empty in practice. Still, we + // prefer to fall back to the default variant rather than panicking + // in case that changes in the future. + let variant = like.map(|info| info.variant).unwrap_or_default(); + + // Select the font. + let index = + self.book.select_fallback(like, variant, c.encode_utf8(&mut [0; 4]))?; + + self.get_or_load(index, db) + } + + /// Tries to retrieve the ID for the index or loads the font, allocating + /// a new ID. + fn get_or_load( + &mut self, + index: usize, + db: &mut Arc, + ) -> Option { + self.to_id + .get(&index) + .copied() + .unwrap_or_else(|| self.load(index, db)) + } + + /// Tries to load the font with the given index in the font book into the + /// database and returns its ID. + fn load( + &mut self, + index: usize, + db: &mut Arc, + ) -> Option { + let font = self.world.font(index)?; + let info = font.info(); + let variant = info.variant; + let id = Arc::make_mut(db).push_face_info(fontdb::FaceInfo { + id: fontdb::ID::dummy(), + source: fontdb::Source::Binary(Arc::new(font.data().clone())), + index: font.index(), + families: vec![( + info.family.clone(), + ttf_parser::Language::English_UnitedStates, + )], + post_script_name: String::new(), + style: match variant.style { + FontStyle::Normal => fontdb::Style::Normal, + FontStyle::Italic => fontdb::Style::Italic, + FontStyle::Oblique => fontdb::Style::Oblique, + }, + weight: fontdb::Weight(variant.weight.to_number()), + stretch: match variant.stretch.round() { + FontStretch::ULTRA_CONDENSED => ttf_parser::Width::UltraCondensed, + FontStretch::EXTRA_CONDENSED => ttf_parser::Width::ExtraCondensed, + FontStretch::CONDENSED => ttf_parser::Width::Condensed, + FontStretch::SEMI_CONDENSED => ttf_parser::Width::SemiCondensed, + FontStretch::NORMAL => ttf_parser::Width::Normal, + FontStretch::SEMI_EXPANDED => ttf_parser::Width::SemiExpanded, + FontStretch::EXPANDED => ttf_parser::Width::Expanded, + FontStretch::EXTRA_EXPANDED => ttf_parser::Width::ExtraExpanded, + FontStretch::ULTRA_EXPANDED => ttf_parser::Width::UltraExpanded, + _ => unreachable!(), + }, + monospaced: info.flags.contains(FontFlags::MONOSPACE), + }); + + font.hash(&mut self.hasher); + + self.to_id.insert(index, Some(id)); + self.from_id.insert(id, font); + + Some(id) + } }