Bump SVG & PDF cinematic universe (#4316)

This commit is contained in:
Laurenz 2024-06-06 17:30:49 +02:00 committed by GitHub
parent 681badf76a
commit 8f7ba8d495
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 404 additions and 408 deletions

188
Cargo.lock generated
View File

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

View File

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

View File

@ -90,12 +90,12 @@ pub fn write_images(context: &WithGlobalRefs) -> (PdfChunk, HashMap<Image, Ref>)
}
}
}
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<EncodedImage>, Option<ColorSpac
EncodedImage::Raster { data, filter, has_color, width, height, icc, alpha }
}
ImageKind::Svg(svg) => 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<u8>, 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),
}

View File

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

View File

@ -72,15 +72,14 @@ fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
}
// 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))
}

View File

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

View File

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

View File

@ -141,6 +141,12 @@ impl Debug for FontWeight {
}
}
impl From<fontdb::Weight> 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()

View File

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

View File

@ -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<f64>,
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<SvgImage> {
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<dyn World + '_>,
families: &[String],
) -> StrResult<SvgImage> {
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: <https://github.com/RazrFalcon/resvg/issues/544>
pub unsafe fn with<F>(&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<usvg::Options> = 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<dyn World + '_>,
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::<usize, Option<String>>::new();
// Loads a font into the database and return it's usvg-compatible name.
let mut load_into_db = |id: usize| -> Option<String> {
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<F>(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<f64> {
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<usize, Option<fontdb::ID>>,
/// The reverse mapping.
from_id: HashMap<fontdb::ID, Font>,
/// Accumulates a hash of all used fonts.
hasher: SipHasher13,
}
/// A synchronized wrapper around a `usvg::Tree`.
pub struct SyncTree(Mutex<usvg::Tree>);
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<F>(&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<fontdb::Database>,
) -> Option<fontdb::ID> {
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<fontdb::Database>,
) -> Option<fontdb::ID> {
// 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<fontdb::Database>,
) -> Option<fontdb::ID> {
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<fontdb::Database>,
) -> Option<fontdb::ID> {
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)
}
}