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>
This commit is contained in:
Martin Robinson 2024-07-11 06:25:38 +02:00 committed by GitHub
parent c6cb7ee981
commit 4907e89656
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 261 additions and 209 deletions

4
Cargo.lock generated
View file

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

View file

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

View file

@ -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,7 +496,111 @@ impl<'a> CanvasData<'a> {
}
}
// https://html.spec.whatwg.org/multipage/#text-preparation-algorithm
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: Replace all ASCII whitespace in text with U+0020 SPACE characters.
let text = replace_ascii_whitespace(text);
// > 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_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;
};
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)
}
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;
}
}
// > 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: 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(
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,
@ -464,77 +609,21 @@ impl<'a> CanvasData<'a> {
max_width: Option<f64>,
is_rtl: bool,
) {
// Step 2.
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.");
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.),
};
// Step 7.
let start = self.text_origin(x as f32, y as f32, &font.metrics(), width, is_rtl);
// TODO: Bidi text layout
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 8.
self.drawtarget.fill_text(
&font,
point_size,
&text,
start,
&self.state.fill_style,
&self.state.draw_options,
);
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
}

View file

@ -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 advance = 0.;
for run in text_runs.iter() {
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);
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;
},
};
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.;
}
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,
point_size,
&font,
run.font.descriptor.pt_size.to_f32_px(),
&ids,
&positions,
&pattern.source(),
options.as_raqote(),
draw_options.as_raqote(),
);
})
}
}
fn fill_rect(

View file

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

View file

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