diff options
author | Martin Robinson <mrobinson@igalia.com> | 2024-07-11 06:25:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-11 04:25:38 +0000 |
commit | 4907e896560dd68bfcd9318b4493de10d7ceee19 (patch) | |
tree | 9af64c74ca7fcd240bba12e6cd8841db31ddfccb | |
parent | c6cb7ee98169ce1acb3b43b5071385d8f4f4adc2 (diff) | |
download | servo-4907e896560dd68bfcd9318b4493de10d7ceee19.tar.gz servo-4907e896560dd68bfcd9318b4493de10d7ceee19.zip |
canvas: Remove as much usage of `font-kit` as possible (#32758)
Do font selection using Servo's font backend, which is shared with the
rest of layout. In addition, delay the creation of the `font-kit` font
until just before rendering with `raqote`. The idea is that when
`raqote` is no longer used, we can drop the `font-kit` dependency.
This change has the side-effect of fixing text rendering in canvas,
adding support for font fallback in canvas, and also correcting a bug in
font selection with size overrides.
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
-rw-r--r-- | Cargo.lock | 4 | ||||
-rw-r--r-- | components/canvas/Cargo.toml | 4 | ||||
-rw-r--r-- | components/canvas/canvas_data.rs | 325 | ||||
-rw-r--r-- | components/canvas/raqote_backend.rs | 105 | ||||
-rw-r--r-- | components/fonts/font.rs | 4 | ||||
-rw-r--r-- | components/fonts/font_context.rs | 6 |
6 files changed, 250 insertions, 198 deletions
diff --git a/Cargo.lock b/Cargo.lock index 36f9d03f1f0..f8d71891b73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,6 +626,7 @@ dependencies = [ name = "canvas" version = "0.0.1" dependencies = [ + "app_units", "bitflags 2.6.0", "byteorder", "canvas_traits", @@ -641,8 +642,10 @@ dependencies = [ "lyon_geom", "net_traits", "num-traits", + "parking_lot", "pathfinder_geometry", "pixels", + "range", "raqote", "servo_arc", "sparkle", @@ -650,6 +653,7 @@ dependencies = [ "style_traits", "surfman", "time 0.1.45", + "unicode-script", "webrender", "webrender_api", "webrender_traits", diff --git a/components/canvas/Cargo.toml b/components/canvas/Cargo.toml index 80ac1944567..ff0b28af0cb 100644 --- a/components/canvas/Cargo.toml +++ b/components/canvas/Cargo.toml @@ -15,6 +15,7 @@ webgl_backtrace = ["canvas_traits/webgl_backtrace"] xr-profile = ["webxr-api/profile", "time"] [dependencies] +app_units = { workspace = true } bitflags = { workspace = true } byteorder = { workspace = true } canvas_traits = { workspace = true } @@ -30,8 +31,10 @@ log = { workspace = true } lyon_geom = "1.0.4" net_traits = { workspace = true } num-traits = { workspace = true } +parking_lot = { workspace = true } pathfinder_geometry = "0.5" pixels = { path = "../pixels" } +range = { path = "../range" } raqote = "0.8.4" servo_arc = { workspace = true } sparkle = { workspace = true } @@ -39,6 +42,7 @@ style = { workspace = true } style_traits = { workspace = true } surfman = { workspace = true } time = { workspace = true, optional = true } +unicode-script = { workspace = true } webrender = { workspace = true } webrender_api = { workspace = true } webrender_traits = { workspace = true } diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index b7d878ef35f..68adda8883b 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -5,23 +5,21 @@ use std::mem; use std::sync::Arc; +use app_units::Au; use canvas_traits::canvas::*; use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; -use euclid::{point2, vec2}; -use font_kit::family_name::FamilyName; -use font_kit::font::Font; -use font_kit::metrics::Metrics; -use font_kit::properties::{Properties, Stretch, Style, Weight}; -use font_kit::source::SystemSource; -use fonts::{FontCacheThread, FontContext, FontTemplateRefMethods}; +use euclid::point2; +use fonts::{ + FontCacheThread, FontContext, FontMetrics, FontRef, GlyphStore, ShapingFlags, ShapingOptions, + LAST_RESORT_GLYPH_ADVANCE, +}; use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; -use log::{debug, error, warn}; +use log::{debug, warn}; use num_traits::ToPrimitive; use servo_arc::Arc as ServoArc; use style::color::AbsoluteColor; use style::properties::style_structs::Font as FontStyleStruct; -use style::values::computed::font; -use style_traits::values::ToCss; +use unicode_script::Script; use webrender_api::units::{DeviceIntSize, RectExt as RectExt_}; use webrender_api::{ImageData, ImageDescriptor, ImageDescriptorFlags, ImageFormat, ImageKey}; use webrender_traits::ImageUpdate; @@ -232,10 +230,55 @@ impl<'a> PathBuilderRef<'a> { } } -// TODO(pylbrecht) -// This defines required methods for DrawTarget of azure and raqote -// The prototypes are derived from azure's methods. -// TODO: De-abstract now that Azure is removed? +#[derive(Debug, Default)] +struct UnshapedTextRun<'a> { + font: Option<FontRef>, + script: Script, + string: &'a str, +} + +impl<'a> UnshapedTextRun<'a> { + fn script_and_font_compatible(&self, script: Script, other_font: &Option<FontRef>) -> bool { + if self.script != script { + return false; + } + + match (&self.font, other_font) { + (Some(font_a), Some(font_b)) => font_a.identifier() == font_b.identifier(), + (None, None) => true, + _ => false, + } + } + + fn to_shaped_text_run(self) -> Option<TextRun> { + let font = self.font?; + if self.string.is_empty() { + return None; + } + + let word_spacing = Au::from_f64_px( + font.glyph_index(' ') + .map(|glyph_id| font.glyph_h_advance(glyph_id)) + .unwrap_or(LAST_RESORT_GLYPH_ADVANCE), + ); + let options = ShapingOptions { + letter_spacing: None, + word_spacing, + script: self.script, + flags: ShapingFlags::empty(), + }; + let glyphs = font.shape_text(self.string, &options); + Some(TextRun { font, glyphs }) + } +} + +pub struct TextRun { + pub font: FontRef, + pub glyphs: Arc<GlyphStore>, +} + +// This defines required methods for a DrawTarget (currently only implemented for raqote). The +// prototypes are derived from the now-removed Azure backend's methods. pub trait GenericDrawTarget { fn clear_rect(&mut self, rect: &Rect<f32>); fn copy_surface( @@ -268,9 +311,7 @@ pub trait GenericDrawTarget { fn fill(&mut self, path: &Path, pattern: Pattern, draw_options: &DrawOptions); fn fill_text( &mut self, - font: &Font, - point_size: f32, - text: &str, + text_runs: Vec<TextRun>, start: Point2D<f32>, pattern: &Pattern, draw_options: &DrawOptions, @@ -455,86 +496,134 @@ impl<'a> CanvasData<'a> { } } - // https://html.spec.whatwg.org/multipage/#text-preparation-algorithm - pub fn fill_text( + pub fn fill_text_with_size( &mut self, text: String, x: f64, y: f64, max_width: Option<f64>, is_rtl: bool, + size: f64, ) { - // Step 2. + // > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters. let text = replace_ascii_whitespace(text); - // Step 3. - let point_size = self - .state - .font_style - .as_ref() - .map_or(10., |style| style.font_size.computed_size().px()); - let font_style = self.state.font_style.as_ref(); - let font = font_style.map_or_else( - || load_system_font_from_style(None), - |style| { - let font_group = self.font_context.font_group(ServoArc::new(style.clone())); - let font = font_group - .write() - .first(&self.font_context) - .expect("couldn't find font"); - Font::from_bytes(font.template.data(), 0) - .ok() - .or_else(|| load_system_font_from_style(Some(style))) - }, - ); - let font = match font { - Some(f) => f, - None => { - error!("Couldn't load desired font or system fallback."); - return; - }, + // > Step 3: Let font be the current font of target, as given by that object's font + // > attribute. + let Some(ref font_style) = self.state.font_style else { + return; }; - let font_width = font_width(&text, point_size, &font); - - // Step 6. - let max_width = max_width.map(|width| width as f32); - let (width, scale_factor) = match max_width { - Some(max_width) if max_width > font_width => (max_width, 1.), - Some(max_width) => (font_width, max_width / font_width), - None => (font_width, 1.), + + let font_group = self + .font_context + .font_group_with_size(font_style.clone(), Au::from_f64_px(size)); + let mut font_group = font_group.write(); + let Some(first_font) = font_group.first(&self.font_context) else { + warn!("Could not render canvas text, because there was no first font."); + return; }; - // Step 7. - let start = self.text_origin(x as f32, y as f32, &font.metrics(), width, is_rtl); + let mut runs = Vec::new(); + let mut current_text_run = UnshapedTextRun::default(); + let mut current_text_run_start_index = 0; + for (index, character) in text.char_indices() { + // TODO: This should ultimately handle emoji variation selectors, but raqote does not yet + // have support for color glyphs. + let script = Script::from(character); + let font = font_group.find_by_codepoint(&self.font_context, character, None); + + if !current_text_run.script_and_font_compatible(script, &font) { + let previous_text_run = mem::replace( + &mut current_text_run, + UnshapedTextRun { + font: font.clone(), + script, + ..Default::default() + }, + ); + current_text_run_start_index = index; + runs.push(previous_text_run) + } - // TODO: Bidi text layout + current_text_run.string = + &text[current_text_run_start_index..index + character.len_utf8()]; + } + runs.push(current_text_run); + + // TODO: This doesn't do any kind of line layout at all. In particular, there needs + // to be some alignment along a baseline and also support for bidi text. + let shaped_runs: Vec<_> = runs + .into_iter() + .filter_map(UnshapedTextRun::to_shaped_text_run) + .collect(); + let total_advance = shaped_runs + .iter() + .map(|run| run.glyphs.total_advance()) + .sum::<Au>() + .to_f64_px(); + + // > Step 6: If maxWidth was provided and the hypothetical width of the inline box in the + // > hypothetical line box is greater than maxWidth CSS pixels, then change font to have a + // > more condensed font (if one is available or if a reasonably readable one can be + // > synthesized by applying a horizontal scale factor to the font) or a smaller font, and + // > return to the previous step. + // + // TODO: We only try decreasing the font size here. Eventually it would make sense to use + // other methods to try to decrease the size, such as finding a narrower font or decreasing + // spacing. + if let Some(max_width) = max_width { + let new_size = (max_width / total_advance * size).floor().max(5.); + if total_advance > max_width && new_size != size { + self.fill_text_with_size(text, x, y, Some(max_width), is_rtl, new_size); + return; + } + } - let old_transform = self.get_transform(); - self.set_transform( - &old_transform - .pre_translate(vec2(start.x, 0.)) - .pre_scale(scale_factor, 1.) - .pre_translate(vec2(-start.x, 0.)), + // > Step 7: Find the anchor point for the line of text. + let start = self.find_anchor_point_for_line_of_text( + x as f32, + y as f32, + &first_font.metrics, + total_advance as f32, + is_rtl, ); - // Step 8. + // > Step 8: Let result be an array constructed by iterating over each glyph in the inline box + // > from left to right (if any), adding to the array, for each glyph, the shape of the glyph + // > as it is in the inline box, positioned on a coordinate space using CSS pixels with its + // > origin is at the anchor point. self.drawtarget.fill_text( - &font, - point_size, - &text, + shaped_runs, start, &self.state.fill_style, &self.state.draw_options, ); + } + + /// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm> + pub fn fill_text( + &mut self, + text: String, + x: f64, + y: f64, + max_width: Option<f64>, + is_rtl: bool, + ) { + let Some(ref font_style) = self.state.font_style else { + return; + }; - self.set_transform(&old_transform); + let size = font_style.font_size.computed_size(); + self.fill_text_with_size(text, x, y, max_width, is_rtl, size.px() as f64); } - fn text_origin( + /// Find the *anchor_point* for the given parameters of a line of text. + /// See <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>. + fn find_anchor_point_for_line_of_text( &self, x: f32, y: f32, - metrics: &Metrics, + metrics: &FontMetrics, width: f32, is_rtl: bool, ) -> Point2D<f32> { @@ -551,13 +640,15 @@ impl<'a> CanvasData<'a> { _ => 0., }; + let ascent = metrics.ascent.to_f32_px(); + let descent = metrics.descent.to_f32_px(); let anchor_y = match self.state.text_baseline { - TextBaseline::Top => metrics.ascent, - TextBaseline::Hanging => metrics.ascent * HANGING_BASELINE_DEFAULT, - TextBaseline::Ideographic => -metrics.descent * IDEOGRAPHIC_BASELINE_DEFAULT, - TextBaseline::Middle => (metrics.ascent - metrics.descent) / 2., + TextBaseline::Top => ascent, + TextBaseline::Hanging => ascent * HANGING_BASELINE_DEFAULT, + TextBaseline::Ideographic => -descent * IDEOGRAPHIC_BASELINE_DEFAULT, + TextBaseline::Middle => (ascent - descent) / 2., TextBaseline::Alphabetic => 0., - TextBaseline::Bottom => -metrics.descent, + TextBaseline::Bottom => -descent, }; point2(x + anchor_x, y + anchor_y) @@ -1140,7 +1231,7 @@ impl<'a> CanvasData<'a> { } pub fn set_font(&mut self, font_style: FontStyleStruct) { - self.state.font_style = Some(font_style) + self.state.font_style = Some(ServoArc::new(font_style)) } pub fn set_text_align(&mut self, text_align: TextAlign) { @@ -1239,7 +1330,7 @@ pub struct CanvasPaintState<'a> { pub shadow_offset_y: f64, pub shadow_blur: f64, pub shadow_color: Color, - pub font_style: Option<FontStyleStruct>, + pub font_style: Option<ServoArc<FontStyleStruct>>, pub text_align: TextAlign, pub text_baseline: TextBaseline, } @@ -1330,71 +1421,6 @@ impl RectExt for Rect<u32> { } } -fn to_font_kit_family(font_family: &font::SingleFontFamily) -> FamilyName { - match font_family { - font::SingleFontFamily::FamilyName(family_name) => { - FamilyName::Title(family_name.to_css_string()) - }, - font::SingleFontFamily::Generic(generic) => match generic { - font::GenericFontFamily::Serif => FamilyName::Serif, - font::GenericFontFamily::SansSerif => FamilyName::SansSerif, - font::GenericFontFamily::Monospace => FamilyName::Monospace, - font::GenericFontFamily::Fantasy => FamilyName::Fantasy, - font::GenericFontFamily::Cursive => FamilyName::Cursive, - // TODO: There is no FontFamily::SystemUi. - font::GenericFontFamily::SystemUi => unreachable!("system-ui should be disabled"), - font::GenericFontFamily::None => unreachable!("Shouldn't appear in computed values"), - }, - } -} - -fn load_system_font_from_style(font_style: Option<&FontStyleStruct>) -> Option<Font> { - let mut properties = Properties::new(); - let style = match font_style { - Some(style) => style, - None => return load_default_system_fallback_font(&properties), - }; - let family_names = style - .font_family - .families - .iter() - .map(to_font_kit_family) - .collect::<Vec<_>>(); - let properties = properties - .style(match style.font_style { - font::FontStyle::NORMAL => Style::Normal, - font::FontStyle::ITALIC => Style::Italic, - _ => { - // TODO: support oblique angle. - Style::Oblique - }, - }) - .weight(Weight(style.font_weight.value())) - .stretch(Stretch(style.font_stretch.to_percentage().0)); - let font_handle = match SystemSource::new().select_best_match(&family_names, properties) { - Ok(handle) => handle, - Err(e) => { - error!("error getting font handle for style {:?}: {}", style, e); - return load_default_system_fallback_font(properties); - }, - }; - match font_handle.load() { - Ok(f) => Some(f), - Err(e) => { - error!("error loading font for style {:?}: {}", style, e); - load_default_system_fallback_font(properties) - }, - } -} - -fn load_default_system_fallback_font(properties: &Properties) -> Option<Font> { - SystemSource::new() - .select_best_match(&[FamilyName::SansSerif], properties) - .ok()? - .load() - .ok() -} - fn replace_ascii_whitespace(text: String) -> String { text.chars() .map(|c| match c { @@ -1403,18 +1429,3 @@ fn replace_ascii_whitespace(text: String) -> String { }) .collect() } - -// TODO: This currently calculates the width using just advances and doesn't -// determine the fallback font in case a character glyph isn't found. -fn font_width(text: &str, point_size: f32, font: &Font) -> f32 { - let metrics = font.metrics(); - let mut width = 0.; - for c in text.chars() { - if let Some(glyph_id) = font.glyph_for_char(c) { - if let Ok(advance) = font.advance(glyph_id) { - width += advance.x() * point_size / metrics.units_per_em as f32; - } - } - } - width -} diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index 8a9223bb191..359b0271368 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -2,23 +2,36 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cell::RefCell; +use std::collections::HashMap; + use canvas_traits::canvas::*; use cssparser::color::clamp_unit_f32; use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; use euclid::Angle; use font_kit::font::Font; +use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods}; use log::warn; use lyon_geom::Arc; +use range::Range; use raqote::PathOp; use style::color::AbsoluteColor; -use crate::canvas_data; use crate::canvas_data::{ - Backend, CanvasPaintState, Color, CompositionOp, DrawOptions, Filter, GenericDrawTarget, - GenericPathBuilder, GradientStop, GradientStops, Path, SourceSurface, StrokeOptions, + self, Backend, CanvasPaintState, Color, CompositionOp, DrawOptions, Filter, GenericDrawTarget, + GenericPathBuilder, GradientStop, GradientStops, Path, SourceSurface, StrokeOptions, TextRun, }; use crate::canvas_paint_thread::AntialiasMode; +thread_local! { + /// The shared font cache used by all canvases that render on a thread. It would be nicer + /// to have a global cache, but it looks like font-kit uses a per-thread FreeType, so + /// in order to ensure that fonts are particular to a thread we have to make our own + /// cache thread local as well. + static SHARED_FONT_CACHE: RefCell<HashMap<FontIdentifier, Font>> = RefCell::default(); +} + +#[derive(Default)] pub struct RaqoteBackend; impl Backend for RaqoteBackend { @@ -508,43 +521,61 @@ impl GenericDrawTarget for raqote::DrawTarget { fn fill_text( &mut self, - font: &Font, - point_size: f32, - text: &str, + text_runs: Vec<TextRun>, start: Point2D<f32>, pattern: &canvas_data::Pattern, - options: &DrawOptions, + draw_options: &DrawOptions, ) { - let mut start = pathfinder_geometry::vector::vec2f(start.x, start.y); - let mut ids = Vec::new(); - let mut positions = Vec::new(); - for c in text.chars() { - let id = match font.glyph_for_char(c) { - Some(id) => id, - None => { - warn!("Skipping non-existent glyph {}", c); - continue; - }, - }; - ids.push(id); - positions.push(Point2D::new(start.x(), start.y())); - let advance = match font.advance(id) { - Ok(advance) => advance, - Err(e) => { - warn!("Skipping glyph {} with missing advance: {:?}", c, e); - continue; - }, - }; - start += advance * point_size / 24. / 96.; - } - self.draw_glyphs( - font, - point_size, - &ids, - &positions, - &pattern.source(), - options.as_raqote(), - ); + let mut advance = 0.; + for run in text_runs.iter() { + let mut positions = Vec::new(); + let glyphs = &run.glyphs; + let ids: Vec<_> = glyphs + .iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len())) + .map(|glyph| { + let glyph_offset = glyph.offset().unwrap_or(Point2D::zero()); + positions.push(Point2D::new( + advance + start.x + glyph_offset.x.to_f32_px(), + start.y + glyph_offset.y.to_f32_px(), + )); + advance += glyph.advance().to_f32_px(); + glyph.id() + }) + .collect(); + + // TODO: raqote uses font-kit to rasterize glyphs, but font-kit fails an assertion when + // using color bitmap fonts in the FreeType backend. For now, simply do not render these + // type of fonts. + if run.font.has_color_bitmap_or_colr_table() { + continue; + } + + let template = &run.font.template; + + SHARED_FONT_CACHE.with(|font_cache| { + let identifier = template.identifier(); + if !font_cache.borrow().contains_key(&identifier) { + let Ok(font) = Font::from_bytes(template.data(), identifier.index()) else { + return; + }; + font_cache.borrow_mut().insert(identifier.clone(), font); + } + + let font_cache = font_cache.borrow(); + let Some(font) = font_cache.get(&identifier) else { + return; + }; + + self.draw_glyphs( + &font, + run.font.descriptor.pt_size.to_f32_px(), + &ids, + &positions, + &pattern.source(), + draw_options.as_raqote(), + ); + }) + } } fn fill_rect( diff --git a/components/fonts/font.rs b/components/fonts/font.rs index dd3cbe7d2cc..719ec70f144 100644 --- a/components/fonts/font.rs +++ b/components/fonts/font.rs @@ -480,9 +480,7 @@ pub struct FontGroup { } impl FontGroup { - pub fn new(style: &FontStyleStruct) -> FontGroup { - let descriptor = FontDescriptor::from(style); - + pub fn new(style: &FontStyleStruct, descriptor: FontDescriptor) -> FontGroup { let families: SmallVec<[FontGroupFamily; 8]> = style .font_family .families diff --git a/components/fonts/font_context.rs b/components/fonts/font_context.rs index efadb035067..140fa56f14d 100644 --- a/components/fonts/font_context.rs +++ b/components/fonts/font_context.rs @@ -681,7 +681,11 @@ impl<FCT: FontSource> CachingFontSource<FCT> { if let Some(font_group) = self.resolved_font_groups.read().get(&cache_key) { return font_group.clone(); } - let font_group = Arc::new(RwLock::new(FontGroup::new(&cache_key.style))); + + let mut descriptor = FontDescriptor::from(&*cache_key.style); + descriptor.pt_size = size; + + let font_group = Arc::new(RwLock::new(FontGroup::new(&cache_key.style, descriptor))); self.resolved_font_groups .write() .insert(cache_key, font_group.clone()); |