diff --git a/Cargo.lock b/Cargo.lock index b30b84967..7625ddeee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.16.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +checksum = "6d2894eb653f564384ae8da32b78d9c8db0394715525d917328e435b9babd902" dependencies = [ "fontconfig-parser", "log", @@ -248,9 +248,9 @@ checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "rustybuzz" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88117946aa1bfb53c2ae0643ceac6506337f44887f8c9fbfb43587b1cc52ba49" +checksum = "7730060ad401b0d1807c904ea56735288af101430aa0d2ab8358b789f5f37002" dependencies = [ "bitflags 2.5.0", "bytemuck", @@ -360,9 +360,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[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 = "unicode-bidi" diff --git a/crates/resvg/tests/fonts/NotoColorEmojiCOLR.subset.ttf b/crates/resvg/tests/fonts/NotoColorEmojiCOLR.subset.ttf new file mode 100644 index 000000000..4199dc0f8 Binary files /dev/null and b/crates/resvg/tests/fonts/NotoColorEmojiCOLR.subset.ttf differ diff --git a/crates/resvg/tests/fonts/README.md b/crates/resvg/tests/fonts/README.md index 55bb6199f..7108843b6 100644 --- a/crates/resvg/tests/fonts/README.md +++ b/crates/resvg/tests/fonts/README.md @@ -6,4 +6,12 @@ Twitter Color Emoji Noto Color Emoji (CBDT) 1. Download: https://github.com/googlefonts/noto-emoji/blob/main/fonts/NotoColorEmoji.ttf -2. Run `fonttools subset NotoColorEmoji.ttf --unicodes="U+1F600" --output-file=NotoColorEmojiCBDT.subset.ttf` \ No newline at end of file +2. Run `fonttools subset NotoColorEmoji.ttf --unicodes="U+1F600" --output-file=NotoColorEmojiCBDT.subset.ttf` + +Noto COLOR Emoji (COLRv1) +1. Download: https://fonts.google.com/noto/specimen/Noto+Color+Emoji +2. Run `fonttools subset NotoColorEmoji-Regular.ttf --unicodes="U+1F436,U+1F41D,U+1F313,U+1F973" --output-file=NotoColorEmojiCOLR.subset.ttf` +3. Run `fonttools ttx NotoColorEmojiCOLR.subset.ttf` +4. Go to the section and rename all instances of "Noto Color Emoji" to "Noto Color Emoji COLR" (so that +we can distinguish them from CBDT in tests). +5. Run `fonttools ttx -f NotoColorEmojiCOLR.subset.ttx` \ No newline at end of file diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index 7d19d937e..6dfcb61da 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1363,6 +1363,7 @@ use crate::render; #[test] fn text_baseline_shift_with_rotate() { assert_eq!(render("tests/text/baseline-shift/with-rotate"), 0); } #[test] fn text_color_font_cbdt() { assert_eq!(render("tests/text/color-font/cbdt"), 0); } #[test] fn text_color_font_colrv0() { assert_eq!(render("tests/text/color-font/colrv0"), 0); } +#[test] fn text_color_font_colrv1() { assert_eq!(render("tests/text/color-font/colrv1"), 0); } #[test] fn text_color_font_compound_emojis_and_coordinates_list() { assert_eq!(render("tests/text/color-font/compound-emojis-and-coordinates-list"), 0); } #[test] fn text_color_font_compound_emojis() { assert_eq!(render("tests/text/color-font/compound-emojis"), 0); } #[test] fn text_color_font_mixed_text_rtl() { assert_eq!(render("tests/text/color-font/mixed-text-rtl"), 0); } diff --git a/crates/resvg/tests/tests/text/color-font/colrv1.png b/crates/resvg/tests/tests/text/color-font/colrv1.png new file mode 100644 index 000000000..957e447e9 Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/colrv1.png differ diff --git a/crates/resvg/tests/tests/text/color-font/colrv1.svg b/crates/resvg/tests/tests/text/color-font/colrv1.svg new file mode 100644 index 000000000..006340acc --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/colrv1.svg @@ -0,0 +1,12 @@ + + `COLRv1` + + + + πŸΆπŸπŸŒ“πŸ₯³ + + + + diff --git a/crates/usvg/Cargo.toml b/crates/usvg/Cargo.toml index 525de110d..9b5859a78 100644 --- a/crates/usvg/Cargo.toml +++ b/crates/usvg/Cargo.toml @@ -36,8 +36,8 @@ simplecss = "0.2" siphasher = "1.0" # perfect hash implementation # text -fontdb = { version = "0.16.1", default-features = false, optional = true } -rustybuzz = { version = "0.13", optional = true } +fontdb = { version = "0.17.0", default-features = false, optional = true } +rustybuzz = { version = "0.14.0", optional = true } unicode-bidi = { version = "0.3", optional = true } unicode-script = { version = "0.5", optional = true } unicode-vo = { version = "0.1", optional = true } diff --git a/crates/usvg/src/text/colr.rs b/crates/usvg/src/text/colr.rs new file mode 100644 index 000000000..fe4b091e3 --- /dev/null +++ b/crates/usvg/src/text/colr.rs @@ -0,0 +1,358 @@ +use crate::parser::OptionLog; +use rustybuzz::ttf_parser; + +struct Builder<'a>(&'a mut String); + +impl Builder<'_> { + fn finish(&mut self) { + if !self.0.is_empty() { + self.0.pop(); // remove trailing space + } + } +} + +impl ttf_parser::OutlineBuilder for Builder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "M {} {} ", x, y).unwrap() + } + + fn line_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "L {} {} ", x, y).unwrap() + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap() + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap() + } + + fn close(&mut self) { + self.0.push_str("Z ") + } +} + +trait XmlWriterExt { + fn write_color_attribute(&mut self, name: &str, ts: ttf_parser::RgbaColor); + fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform); + fn write_spread_method_attribute(&mut self, method: ttf_parser::colr::GradientExtend); +} + +impl XmlWriterExt for xmlwriter::XmlWriter { + fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) { + self.write_attribute_fmt( + name, + format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), + ); + } + + fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) { + if ts.is_default() { + return; + } + + self.write_attribute_fmt( + name, + format_args!( + "matrix({} {} {} {} {} {})", + ts.a, ts.b, ts.c, ts.d, ts.e, ts.f + ), + ); + } + + fn write_spread_method_attribute(&mut self, extend: ttf_parser::colr::GradientExtend) { + self.write_attribute( + "spreadMethod", + match extend { + ttf_parser::colr::GradientExtend::Pad => &"pad", + ttf_parser::colr::GradientExtend::Repeat => &"repeat", + ttf_parser::colr::GradientExtend::Reflect => &"reflect", + }, + ); + } +} + +// NOTE: This is only a best-effort translation of COLR into SVG. +pub(crate) struct GlyphPainter<'a> { + pub(crate) face: &'a ttf_parser::Face<'a>, + pub(crate) svg: &'a mut xmlwriter::XmlWriter, + pub(crate) path_buf: &'a mut String, + pub(crate) gradient_index: usize, + pub(crate) clip_path_index: usize, + pub(crate) palette_index: u16, + pub(crate) transform: ttf_parser::Transform, + pub(crate) outline_transform: ttf_parser::Transform, + pub(crate) transforms_stack: Vec, +} + +impl<'a> GlyphPainter<'a> { + fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) { + for stop in stops { + self.svg.start_element("stop"); + self.svg.write_attribute("offset", &stop.stop_offset); + self.svg.write_color_attribute("stop-color", stop.color); + let opacity = f32::from(stop.color.alpha) / 255.0; + self.svg.write_attribute("stop-opacity", &opacity); + self.svg.end_element(); + } + } + + fn paint_solid(&mut self, color: ttf_parser::RgbaColor) { + self.svg.start_element("path"); + self.svg.write_color_attribute("fill", color); + let opacity = f32::from(color.alpha) / 255.0; + self.svg.write_attribute("fill-opacity", &opacity); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) { + let gradient_id = format!("lg{}", self.gradient_index); + self.gradient_index += 1; + + let gradient_transform = paint_transform(self.outline_transform, self.transform); + + // TODO: We ignore x2, y2. Have to apply them somehow. + // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode + // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will + // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and + // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf + // we will see the actual spreadMode. We need to account for that somehow. + self.svg.start_element("linearGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("x1", &gradient.x0); + self.svg.write_attribute("y1", &gradient.y0); + self.svg.write_attribute("x2", &gradient.x1); + self.svg.write_attribute("y2", &gradient.y1); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.svg.write_spread_method_attribute(gradient.extend); + self.svg + .write_transform_attribute("gradientTransform", gradient_transform); + self.write_gradient_stops( + gradient.stops(self.palette_index, self.face.variation_coordinates()), + ); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) { + let gradient_id = format!("rg{}", self.gradient_index); + self.gradient_index += 1; + + self.svg.start_element("radialGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("cx", &gradient.x1); + self.svg.write_attribute("cy", &gradient.y1); + self.svg.write_attribute("r", &gradient.r1); + self.svg.write_attribute("fr", &gradient.r0); + self.svg.write_attribute("fx", &gradient.x0); + self.svg.write_attribute("fy", &gradient.y0); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.svg.write_spread_method_attribute(gradient.extend); + self.svg + .write_transform_attribute("gradientTransform", self.transform); + self.write_gradient_stops( + gradient.stops(self.palette_index, self.face.variation_coordinates()), + ); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) { + println!("Warning: sweep gradients are not supported.") + } +} + +fn paint_transform( + outline_transform: ttf_parser::Transform, + transform: ttf_parser::Transform, +) -> ttf_parser::Transform { + let outline_transform = tiny_skia_path::Transform::from_row( + outline_transform.a, + outline_transform.b, + outline_transform.c, + outline_transform.d, + outline_transform.e, + outline_transform.f, + ); + + let gradient_transform = tiny_skia_path::Transform::from_row( + transform.a, + transform.b, + transform.c, + transform.d, + transform.e, + transform.f, + ); + + let gradient_transform = outline_transform + .invert() + .log_none(|| log::warn!("Failed to calculate transform for gradient in glyph.")) + .unwrap_or_default() + .pre_concat(gradient_transform); + + ttf_parser::Transform { + a: gradient_transform.sx, + b: gradient_transform.ky, + c: gradient_transform.kx, + d: gradient_transform.sy, + e: gradient_transform.tx, + f: gradient_transform.ty, + } +} + +impl GlyphPainter<'_> { + fn clip_with_path(&mut self, path: &str) { + let clip_id = format!("cp{}", self.clip_path_index); + self.clip_path_index += 1; + + self.svg.start_element("clipPath"); + self.svg.write_attribute("id", &clip_id); + self.svg.start_element("path"); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", &path); + self.svg.end_element(); + self.svg.end_element(); + + self.svg.start_element("g"); + self.svg + .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id)); + } +} + +impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> { + fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) { + self.path_buf.clear(); + let mut builder = Builder(self.path_buf); + match self.face.outline_glyph(glyph_id, &mut builder) { + Some(v) => v, + None => return, + }; + builder.finish(); + + // We have to write outline using the current transform. + self.outline_transform = self.transform; + } + + fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) { + self.svg.start_element("g"); + + use ttf_parser::colr::CompositeMode; + // TODO: Need to figure out how to represent the other blend modes + // in SVG. + let mode = match mode { + CompositeMode::SourceOver => "normal", + CompositeMode::Screen => "screen", + CompositeMode::Overlay => "overlay", + CompositeMode::Darken => "darken", + CompositeMode::Lighten => "lighten", + CompositeMode::ColorDodge => "color-dodge", + CompositeMode::ColorBurn => "color-burn", + CompositeMode::HardLight => "hard-light", + CompositeMode::SoftLight => "soft-light", + CompositeMode::Difference => "difference", + CompositeMode::Exclusion => "exclusion", + CompositeMode::Multiply => "multiply", + CompositeMode::Hue => "hue", + CompositeMode::Saturation => "saturation", + CompositeMode::Color => "color", + CompositeMode::Luminosity => "luminosity", + _ => { + println!("Warning: unsupported blend mode: {:?}", mode); + "normal" + } + }; + self.svg.write_attribute_fmt( + "style", + format_args!("mix-blend-mode: {}; isolation: isolate", mode), + ); + } + + fn pop_layer(&mut self) { + self.svg.end_element(); // g + } + + fn push_translate(&mut self, tx: f32, ty: f32) { + self.push_transform(ttf_parser::Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty)); + } + + fn push_scale(&mut self, sx: f32, sy: f32) { + self.push_transform(ttf_parser::Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0)); + } + + fn push_rotate(&mut self, angle: f32) { + let cc = (angle * std::f32::consts::PI).cos(); + let ss = (angle * std::f32::consts::PI).sin(); + self.push_transform(ttf_parser::Transform::new(cc, ss, -ss, cc, 0.0, 0.0)); + } + + fn push_skew(&mut self, skew_x: f32, skew_y: f32) { + let x = (-skew_x * std::f32::consts::PI).tan(); + let y = (skew_y * std::f32::consts::PI).tan(); + self.push_transform(ttf_parser::Transform::new(1.0, y, x, 1.0, 0.0, 0.0)); + } + + fn push_transform(&mut self, transform: ttf_parser::Transform) { + self.transforms_stack.push(self.transform); + self.transform = ttf_parser::Transform::combine(self.transform, transform); + } + + fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) { + match paint { + ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color), + ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg), + ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg), + ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg), + } + } + + fn pop_transform(&mut self) { + if let Some(ts) = self.transforms_stack.pop() { + self.transform = ts + } + } + + fn push_clip(&mut self) { + self.clip_with_path(&self.path_buf.clone()); + } + + fn pop_clip(&mut self) { + self.svg.end_element(); + } + + fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) { + let x_min = clipbox.x_min; + let x_max = clipbox.x_max; + let y_min = clipbox.y_min; + let y_max = clipbox.y_max; + + let clip_path = format!( + "M {} {} L {} {} L {} {} L {} {} Z", + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ); + + self.clip_with_path(&clip_path); + } +} diff --git a/crates/usvg/src/text/flatten.rs b/crates/usvg/src/text/flatten.rs index 249bfe945..fb4eb3aa7 100644 --- a/crates/usvg/src/text/flatten.rs +++ b/crates/usvg/src/text/flatten.rs @@ -7,9 +7,11 @@ use std::sync::Arc; use fontdb::{Database, ID}; use rustybuzz::ttf_parser; -use rustybuzz::ttf_parser::{GlyphId, RasterImageFormat}; +use rustybuzz::ttf_parser::{GlyphId, RasterImageFormat, RgbaColor}; use tiny_skia_path::{NonZeroRect, Size, Transform}; +use xmlwriter::XmlWriter; +use crate::text::colr::GlyphPainter; use crate::*; fn resolve_rendering_mode(text: &Text) -> ShapeRendering { @@ -71,21 +73,14 @@ pub(crate) fn flatten(text: &mut Text, fontdb: &fontdb::Database) -> Option<(Gro let mut span_builder = tiny_skia_path::PathBuilder::new(); for glyph in &span.positioned_glyphs { - // A COLRv0 glyph. Will return a vector of paths that make up the glyph description. - // TODO: Don't use black for foreground color? But not sure whether to use fill or stroke - // color. - if let Some(layers) = fontdb.colr(glyph.font, glyph.id) { - push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); - + // A (best-effort conversion of a) COLR glyph. + if let Some(tree) = fontdb.colr(glyph.font, glyph.id) { let mut group = Group { transform: glyph.colr_transform(), ..Group::empty() }; - - for path in layers { - // TODO: Probably need to update abs_transform of children? - group.children.push(Node::Path(Box::new(path))); - } + // TODO: Probably need to update abs_transform of children? + group.children.push(Node::Group(Box::new(tree.root))); group.calculate_bounding_boxes(); new_children.push(Node::Group(Box::new(group))); @@ -195,7 +190,7 @@ pub(crate) trait DatabaseExt { fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; fn raster(&self, id: ID, glyph_id: GlyphId) -> Option; fn svg(&self, id: ID, glyph_id: GlyphId) -> Option; - fn colr(&self, id: ID, glyph_id: GlyphId) -> Option>; + fn colr(&self, id: ID, glyph_id: GlyphId) -> Option; } pub(crate) struct BitmapImage { @@ -269,73 +264,48 @@ impl DatabaseExt for Database { })? } - fn colr(&self, id: ID, glyph_id: GlyphId) -> Option> { - self.with_face_data(id, |data, face_index| -> Option> { - let font = ttf_parser::Face::parse(data, face_index).ok()?; + fn colr(&self, id: ID, glyph_id: GlyphId) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let face = ttf_parser::Face::parse(data, face_index).ok()?; - let mut paths = vec![]; - let mut glyph_painter = GlyphPainter { - face: &font, - paths: &mut paths, - builder: PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }, - }; + let mut svg = XmlWriter::new(xmlwriter::Options::default()); - font.paint_color_glyph(glyph_id, 0, &mut glyph_painter)?; + svg.start_element("svg"); + svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); + svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); - Some(paths) - })? - } -} - -struct GlyphPainter<'a> { - face: &'a ttf_parser::Face<'a>, - paths: &'a mut Vec, - builder: PathBuilder, -} - -impl ttf_parser::colr::Painter for GlyphPainter<'_> { - fn outline(&mut self, glyph_id: ttf_parser::GlyphId) { - let builder = &mut self.builder; - match self.face.outline_glyph(glyph_id, builder) { - Some(v) => v, - None => return, - }; - } + let mut path_buf = String::with_capacity(256); + let gradient_index = 1; + let clip_path_index = 1; - fn paint_foreground(&mut self) { - self.paint_color(ttf_parser::RgbaColor::new(0, 0, 0, 255)); - } + svg.start_element("g"); - fn paint_color(&mut self, color: ttf_parser::RgbaColor) { - let builder = mem::replace( - &mut self.builder, - PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }, - ); - - if let Some(path) = builder.builder.finish().and_then(|p| { - let fill = Fill { - paint: Paint::Color(Color::new_rgb(color.red, color.green, color.blue)), - opacity: Opacity::new(f32::from(color.alpha) / 255.0).unwrap(), - rule: FillRule::NonZero, - context_element: None, + let mut glyph_painter = GlyphPainter { + face: &face, + svg: &mut svg, + path_buf: &mut path_buf, + gradient_index, + clip_path_index, + palette_index: 0, + transform: ttf_parser::Transform::default(), + outline_transform: ttf_parser::Transform::default(), + transforms_stack: vec![ttf_parser::Transform::default()], }; - Path::new( - String::new(), - Visibility::Visible, - Some(fill), - None, - PaintOrder::FillAndStroke, - ShapeRendering::GeometricPrecision, - Arc::new(p), - Transform::default(), + face.paint_color_glyph( + glyph_id, + 0, + RgbaColor::new(0, 0, 0, 255), + &mut glyph_painter, + )?; + svg.end_element(); + + Tree::from_data( + &svg.end_document().as_bytes(), + &Options::default(), + &fontdb::Database::new(), ) - }) { - self.paths.push(path) - } + .ok() + })? } } diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index 67cebcfca..801981639 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use fontdb::{Database, ID}; use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; use rustybuzz::ttf_parser; -use rustybuzz::ttf_parser::GlyphId; +use rustybuzz::ttf_parser::{GlyphId, Tag}; use strict_num::NonZeroPositiveF32; use svgtypes::FontFamily; use tiny_skia_path::{NonZeroRect, Transform}; @@ -1454,19 +1454,11 @@ fn shape_text_with_font( let mut features = Vec::new(); if small_caps { - features.push(rustybuzz::Feature::new( - rustybuzz::Tag::from_bytes(b"smcp"), - 1, - .., - )); + features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp"), 1, ..)); } if !apply_kerning { - features.push(rustybuzz::Feature::new( - rustybuzz::Tag::from_bytes(b"kern"), - 0, - .., - )); + features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern"), 0, ..)); } let output = rustybuzz::shape(&rb_font, &features, buffer); diff --git a/crates/usvg/src/text/mod.rs b/crates/usvg/src/text/mod.rs index e1679c330..6420df1d5 100644 --- a/crates/usvg/src/text/mod.rs +++ b/crates/usvg/src/text/mod.rs @@ -6,6 +6,7 @@ use crate::Text; mod flatten; +mod colr; /// Provides access to the layout of a text node. pub mod layout; diff --git a/crates/usvg/src/tree/text.rs b/crates/usvg/src/tree/text.rs index b38e57263..7346fbd26 100644 --- a/crates/usvg/src/tree/text.rs +++ b/crates/usvg/src/tree/text.rs @@ -566,6 +566,21 @@ impl Text { } /// Text converted into paths, ready to render. + /// + /// Note that this is only a + /// "best-effort" attempt: The text will be converted into group/paths/image + /// primitives, so that they can be rendered with the existing infrastructure. + /// This process is in general lossless and should lead to correct output, with + /// two notable exceptions: + /// 1. For glyphs based on the `SVG` table, only glyphs that are pure SVG 1.1/2.0 + /// are supported. Glyphs that make use of features in the OpenType specification + /// that are not part of the original SVG specification are not supported. + /// 2. For glyphs based on the `COLR` table, there are a certain number of features + /// that are not (correctly) supported, such as conical + /// gradients, certain gradient transforms and some blend modes. But this shouldn't + /// cause any issues in 95% of the cases, as most of those are edge cases. + /// If the two above are not acceptable, then you will need to implement your own + /// glyph rendering logic based on the layouted glyphs (see the `layouted` method). pub fn flattened(&self) -> &Group { &self.flattened }