canvas: Move font selection and text shaping to script (#38979)

Instead of doing font selection and text shaping in `canvas`, move this
to `script`. This allows canvas to use the shared `Document`
`FontContext`, which has access to web fonts. In addition, ensure that
there is a font style accessible for `OffscreenCanvas` in workers.

Testing: This causes a number of WPT tests to start to pass as web fonts
are
supported on canvas again. In addition, some start to fail as they
expose other
issues:
 - The lack of support for the `Context2D.fontStretch` property
 - Issues with zerosize gradient interpolation.
- Differences between quoted and unquoted font family names. This seems
like
a timing issue with the way we are handling web fonts. The test seems to
be
expecting Local fonts to be available immediately (without waiting for
them
to load). This isn't how Servo works ATM. Seems like an issue with the
test.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-28 03:30:34 -07:00 committed by GitHub
parent 91b27d98a2
commit cb64def7e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 604 additions and 809 deletions

View file

@ -3,14 +3,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use canvas_traits::canvas::{
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions,
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions, TextRun,
};
use compositing_traits::SerializableImageData;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use pixels::Snapshot;
use webrender_api::ImageDescriptor;
use crate::canvas_data::{Filter, TextRun};
use crate::canvas_data::Filter;
// This defines required methods for a DrawTarget. The prototypes are derived from the now-removed
// Azure backend's methods.
@ -55,7 +55,6 @@ pub(crate) trait GenericDrawTarget {
fn fill_text(
&mut self,
text_runs: Vec<TextRun>,
start: Point2D<f32>,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f64>,

View file

@ -2,22 +2,10 @@
* 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::mem;
use std::sync::Arc;
use app_units::Au;
use canvas_traits::canvas::*;
use compositing_traits::CrossProcessCompositorApi;
use euclid::default::{Box2D, Point2D, Rect, Size2D, Transform2D, Vector2D};
use euclid::point2;
use fonts::{
ByteIndex, FontBaseline, FontContext, FontGroup, FontMetrics, FontRef, GlyphInfo, GlyphStore,
LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions,
};
use log::warn;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use pixels::Snapshot;
use range::Range;
use unicode_script::Script;
use webrender_api::ImageKey;
use crate::backend::GenericDrawTarget;
@ -26,76 +14,6 @@ use crate::backend::GenericDrawTarget;
// https://github.com/servo/webrender/blob/main/webrender/src/texture_cache.rs#L1475
const MIN_WR_IMAGE_SIZE: Size2D<u64> = Size2D::new(1, 1);
#[derive(Default)]
struct UnshapedTextRun<'a> {
font: Option<FontRef>,
script: Script,
string: &'a str,
}
impl UnshapedTextRun<'_> {
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 into_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(crate) struct TextRun {
pub(crate) font: FontRef,
pub(crate) glyphs: Arc<GlyphStore>,
}
impl TextRun {
fn bounding_box(&self) -> Rect<f32> {
let mut bounding_box = None;
let mut bounds_offset: f32 = 0.;
let glyph_ids = self
.glyphs
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), self.glyphs.len()))
.map(GlyphInfo::id);
for glyph_id in glyph_ids {
let bounds = self.font.typographic_bounds(glyph_id);
let amount = Vector2D::new(bounds_offset, 0.);
let bounds = bounds.translate(amount);
let initiated_bbox = bounding_box.get_or_insert_with(|| {
let origin = Point2D::new(bounds.min_x(), 0.);
Box2D::new(origin, origin).to_rect()
});
bounding_box = Some(initiated_bbox.union(&bounds));
bounds_offset = bounds.max_x();
}
bounding_box.unwrap_or_default()
}
}
#[derive(Clone, Copy)]
pub(crate) enum Filter {
Bilinear,
@ -106,14 +24,12 @@ pub(crate) struct CanvasData<DrawTarget: GenericDrawTarget> {
drawtarget: DrawTarget,
compositor_api: CrossProcessCompositorApi,
image_key: ImageKey,
font_context: Arc<FontContext>,
}
impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
pub(crate) fn new(
size: Size2D<u64>,
compositor_api: CrossProcessCompositorApi,
font_context: Arc<FontContext>,
) -> CanvasData<DrawTarget> {
let size = size.max(MIN_WR_IMAGE_SIZE);
let mut draw_target = DrawTarget::new(size.cast());
@ -124,7 +40,6 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
drawtarget: draw_target,
compositor_api,
image_key,
font_context,
}
}
@ -181,296 +96,26 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fill_text_with_size(
&mut self,
text: String,
x: f64,
y: f64,
max_width: Option<f64>,
is_rtl: bool,
size: f64,
style: FillOrStrokeStyle,
text_options: &TextOptions,
composition_options: CompositionOptions,
transform: Transform2D<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) = text_options.font 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 runs = self.build_unshaped_text_runs(&text, &mut font_group);
// 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::into_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,
style,
text_options,
composition_options,
transform,
);
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,
text_options,
);
// > 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.maybe_bound_shape_with_pattern(
style,
composition_options,
&Rect::from_size(Size2D::new(total_advance, size)),
transform,
|self_, style| {
self_.drawtarget.fill_text(
shaped_runs,
start,
style,
composition_options,
transform,
);
},
);
}
#[allow(clippy::too_many_arguments)]
/// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
pub(crate) fn fill_text(
&mut self,
text: String,
x: f64,
y: f64,
max_width: Option<f64>,
is_rtl: bool,
style: FillOrStrokeStyle,
text_options: TextOptions,
text_bounds: Rect<f64>,
text_runs: Vec<TextRun>,
fill_or_stroke_style: FillOrStrokeStyle,
_shadow_options: ShadowOptions,
composition_options: CompositionOptions,
transform: Transform2D<f64>,
) {
let Some(ref font_style) = text_options.font else {
return;
};
let size = font_style.font_size.computed_size();
self.fill_text_with_size(
text,
x,
y,
max_width,
is_rtl,
size.px() as f64,
style,
&text_options,
self.maybe_bound_shape_with_pattern(
fill_or_stroke_style,
composition_options,
&text_bounds,
transform,
);
}
/// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-measuretext>
pub(crate) fn measure_text(&mut self, text: String, text_options: TextOptions) -> TextMetrics {
// > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
let text = replace_ascii_whitespace(text);
let Some(ref font_style) = text_options.font else {
return TextMetrics::default();
};
let font_group = self.font_context.font_group(font_style.clone());
let mut font_group = font_group.write();
let font = font_group
.first(&self.font_context)
.expect("couldn't find font");
let ascent = font.metrics.ascent.to_f32_px();
let descent = font.metrics.descent.to_f32_px();
let runs = self.build_unshaped_text_runs(&text, &mut font_group);
let shaped_runs: Vec<_> = runs
.into_iter()
.filter_map(UnshapedTextRun::into_shaped_text_run)
.collect();
let total_advance = shaped_runs
.iter()
.map(|run| run.glyphs.total_advance())
.sum::<Au>()
.to_f32_px();
let bounding_box = shaped_runs
.iter()
.map(TextRun::bounding_box)
.reduce(|a, b| {
let amount = Vector2D::new(a.max_x(), 0.);
let bounding_box = b.translate(amount);
a.union(&bounding_box)
})
.unwrap_or_default();
let FontBaseline {
ideographic_baseline,
alphabetic_baseline,
hanging_baseline,
} = match font.baseline() {
Some(baseline) => baseline,
None => FontBaseline {
hanging_baseline: ascent * HANGING_BASELINE_DEFAULT,
ideographic_baseline: -descent * IDEOGRAPHIC_BASELINE_DEFAULT,
alphabetic_baseline: 0.,
|self_, style| {
self_
.drawtarget
.fill_text(text_runs, style, composition_options, transform);
},
};
let anchor_x = match text_options.align {
TextAlign::End => total_advance,
TextAlign::Center => total_advance / 2.,
TextAlign::Right => total_advance,
_ => 0.,
};
let anchor_y = match text_options.baseline {
TextBaseline::Top => ascent,
TextBaseline::Hanging => hanging_baseline,
TextBaseline::Ideographic => ideographic_baseline,
TextBaseline::Middle => (ascent - descent) / 2.,
TextBaseline::Alphabetic => alphabetic_baseline,
TextBaseline::Bottom => -descent,
};
TextMetrics {
width: total_advance,
actual_boundingbox_left: anchor_x - bounding_box.min_x(),
actual_boundingbox_right: bounding_box.max_x() - anchor_x,
actual_boundingbox_ascent: bounding_box.max_y() - anchor_y,
actual_boundingbox_descent: anchor_y - bounding_box.min_y(),
font_boundingbox_ascent: ascent - anchor_y,
font_boundingbox_descent: descent + anchor_y,
em_height_ascent: ascent - anchor_y,
em_height_descent: descent + anchor_y,
hanging_baseline: hanging_baseline - anchor_y,
alphabetic_baseline: alphabetic_baseline - anchor_y,
ideographic_baseline: ideographic_baseline - anchor_y,
}
}
fn build_unshaped_text_runs<'b>(
&self,
text: &'b str,
font_group: &mut FontGroup,
) -> Vec<UnshapedTextRun<'b>> {
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.
let script = Script::from(character);
let font = font_group.find_by_codepoint(&self.font_context, character, None, 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);
runs
}
/// 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: &FontMetrics,
width: f32,
is_rtl: bool,
text_options: &TextOptions,
) -> Point2D<f32> {
let text_align = match text_options.align {
TextAlign::Start if is_rtl => TextAlign::Right,
TextAlign::Start => TextAlign::Left,
TextAlign::End if is_rtl => TextAlign::Left,
TextAlign::End => TextAlign::Right,
text_align => text_align,
};
let anchor_x = match text_align {
TextAlign::Center => -width / 2.,
TextAlign::Right => -width,
_ => 0.,
};
let ascent = metrics.ascent.to_f32_px();
let descent = metrics.descent.to_f32_px();
let anchor_y = match text_options.baseline {
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 => -descent,
};
point2(x + anchor_x, y + anchor_y)
);
}
pub(crate) fn fill_rect(
@ -770,9 +415,6 @@ impl<D: GenericDrawTarget> Drop for CanvasData<D> {
}
}
const HANGING_BASELINE_DEFAULT: f32 = 0.8;
const IDEOGRAPHIC_BASELINE_DEFAULT: f32 = 0.5;
/// It writes an image to the destination target
/// draw_target: the destination target where the image_data will be copied
/// image_data: Pixel information of the image to be written. It takes RGBA8
@ -830,12 +472,3 @@ impl RectToi32 for Rect<f64> {
)
}
}
fn replace_ascii_whitespace(text: String) -> String {
text.chars()
.map(|c| match c {
' ' | '\t' | '\n' | '\r' | '\x0C' => '\x20',
_ => c,
})
.collect()
}

View file

@ -4,7 +4,6 @@
use std::borrow::ToOwned;
use std::collections::HashMap;
use std::sync::Arc;
use std::{f32, thread};
use canvas_traits::ConstellationCanvasMsg;
@ -12,11 +11,9 @@ use canvas_traits::canvas::*;
use compositing_traits::CrossProcessCompositorApi;
use crossbeam_channel::{Sender, select, unbounded};
use euclid::default::{Rect, Size2D, Transform2D};
use fonts::{FontContext, SystemFontServiceProxy};
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use log::warn;
use net_traits::ResourceThreads;
use pixels::Snapshot;
use webrender_api::ImageKey;
@ -26,24 +23,14 @@ pub struct CanvasPaintThread {
canvases: HashMap<CanvasId, Canvas>,
next_canvas_id: CanvasId,
compositor_api: CrossProcessCompositorApi,
font_context: Arc<FontContext>,
}
impl CanvasPaintThread {
fn new(
compositor_api: CrossProcessCompositorApi,
system_font_service: Arc<SystemFontServiceProxy>,
resource_threads: ResourceThreads,
) -> CanvasPaintThread {
fn new(compositor_api: CrossProcessCompositorApi) -> CanvasPaintThread {
CanvasPaintThread {
canvases: HashMap::new(),
next_canvas_id: CanvasId(0),
compositor_api: compositor_api.clone(),
font_context: Arc::new(FontContext::new(
system_font_service,
compositor_api,
resource_threads,
)),
}
}
@ -51,8 +38,6 @@ impl CanvasPaintThread {
/// communicate with it.
pub fn start(
compositor_api: CrossProcessCompositorApi,
system_font_service: Arc<SystemFontServiceProxy>,
resource_threads: ResourceThreads,
) -> (Sender<ConstellationCanvasMsg>, IpcSender<CanvasMsg>) {
let (ipc_sender, ipc_receiver) = ipc::channel::<CanvasMsg>().unwrap();
let msg_receiver = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(ipc_receiver);
@ -61,7 +46,7 @@ impl CanvasPaintThread {
.name("Canvas".to_owned())
.spawn(move || {
let mut canvas_paint_thread = CanvasPaintThread::new(
compositor_api, system_font_service, resource_threads);
compositor_api);
loop {
select! {
recv(msg_receiver) -> msg => {
@ -108,7 +93,7 @@ impl CanvasPaintThread {
let canvas_id = self.next_canvas_id;
self.next_canvas_id.0 += 1;
let canvas = Canvas::new(size, self.compositor_api.clone(), self.font_context.clone())?;
let canvas = Canvas::new(size, self.compositor_api.clone())?;
let image_key = canvas.image_key();
self.canvases.insert(canvas_id, canvas);
@ -122,25 +107,17 @@ impl CanvasPaintThread {
fn process_canvas_2d_message(&mut self, message: Canvas2dMsg, canvas_id: CanvasId) {
match message {
Canvas2dMsg::FillText(
text,
x,
y,
max_width,
style,
is_rtl,
text_options,
text_bounds,
text_runs,
fill_or_stroke_style,
shadow_options,
composition_options,
transform,
) => {
self.canvas(canvas_id).fill_text(
text,
x,
y,
max_width,
is_rtl,
style,
text_options,
text_bounds,
text_runs,
fill_or_stroke_style,
shadow_options,
composition_options,
transform,
@ -268,10 +245,6 @@ impl CanvasPaintThread {
transform,
);
},
Canvas2dMsg::MeasureText(text, sender, text_options) => {
let metrics = self.canvas(canvas_id).measure_text(text, text_options);
sender.send(metrics).unwrap();
},
Canvas2dMsg::GetImageData(dest_rect, sender) => {
let snapshot = self.canvas(canvas_id).read_pixels(dest_rect);
sender.send(snapshot.as_ipc()).unwrap();
@ -302,27 +275,17 @@ enum Canvas {
}
impl Canvas {
fn new(
size: Size2D<u64>,
compositor_api: CrossProcessCompositorApi,
font_context: Arc<FontContext>,
) -> Option<Self> {
fn new(size: Size2D<u64>, compositor_api: CrossProcessCompositorApi) -> Option<Self> {
match servo_config::pref!(dom_canvas_backend)
.to_lowercase()
.as_str()
{
#[cfg(feature = "vello_cpu")]
"" | "auto" | "vello_cpu" => Some(Self::VelloCPU(CanvasData::new(
size,
compositor_api,
font_context,
))),
"" | "auto" | "vello_cpu" => {
Some(Self::VelloCPU(CanvasData::new(size, compositor_api)))
},
#[cfg(feature = "vello")]
"" | "auto" | "vello" => Some(Self::Vello(CanvasData::new(
size,
compositor_api,
font_context,
))),
"" | "auto" | "vello" => Some(Self::Vello(CanvasData::new(size, compositor_api))),
s => {
warn!("Unknown 2D canvas backend: `{s}`");
None
@ -348,16 +311,11 @@ impl Canvas {
}
}
#[allow(clippy::too_many_arguments)]
fn fill_text(
&mut self,
text: String,
x: f64,
y: f64,
max_width: Option<f64>,
is_rtl: bool,
style: FillOrStrokeStyle,
text_options: TextOptions,
text_bounds: Rect<f64>,
text_runs: Vec<TextRun>,
fill_or_stroke_style: FillOrStrokeStyle,
shadow_options: ShadowOptions,
composition_options: CompositionOptions,
transform: Transform2D<f64>,
@ -365,26 +323,18 @@ impl Canvas {
match self {
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.fill_text(
text,
x,
y,
max_width,
is_rtl,
style,
text_options,
text_bounds,
text_runs,
fill_or_stroke_style,
shadow_options,
composition_options,
transform,
),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.fill_text(
text,
x,
y,
max_width,
is_rtl,
style,
text_options,
text_bounds,
text_runs,
fill_or_stroke_style,
shadow_options,
composition_options,
transform,
@ -558,15 +508,6 @@ impl Canvas {
}
}
fn measure_text(&mut self, text: String, text_options: TextOptions) -> TextMetrics {
match self {
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.measure_text(text, text_options),
#[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.measure_text(text, text_options),
}
}
fn clip_path(&mut self, path: &Path, fill_rule: FillRule, transform: Transform2D<f64>) {
match self {
#[cfg(feature = "vello")]

View file

@ -18,15 +18,14 @@ use std::rc::Rc;
use canvas_traits::canvas::{
CompositionOptions, CompositionOrBlending, CompositionStyle, FillOrStrokeStyle, FillRule,
LineOptions, Path, ShadowOptions,
LineOptions, Path, ShadowOptions, TextRun,
};
use compositing_traits::SerializableImageData;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods as _};
use fonts::FontIdentifier;
use ipc_channel::ipc::IpcSharedMemory;
use kurbo::Shape as _;
use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
use range::Range;
use vello::wgpu::{
BackendOptions, Backends, Buffer, BufferDescriptor, BufferUsages, COPY_BYTES_PER_ROW_ALIGNMENT,
CommandEncoderDescriptor, Device, Extent3d, Instance, InstanceDescriptor, InstanceFlags,
@ -38,7 +37,7 @@ use vello::{kurbo, peniko};
use webrender_api::{ImageDescriptor, ImageDescriptorFlags};
use crate::backend::{Convert as _, GenericDrawTarget};
use crate::canvas_data::{Filter, TextRun};
use crate::canvas_data::Filter;
thread_local! {
/// The shared font cache used by all canvases that render on a thread.
@ -381,7 +380,6 @@ impl GenericDrawTarget for VelloDrawTarget {
fn fill_text(
&mut self,
text_runs: Vec<TextRun>,
start: Point2D<f32>,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f64>,
@ -390,24 +388,19 @@ impl GenericDrawTarget for VelloDrawTarget {
let pattern = convert_to_brush(style, composition_options);
let transform = transform.cast().into();
self.with_composition(composition_options.composition_operation, |self_| {
let mut advance = 0.;
for run in text_runs.iter() {
let glyphs = &run.glyphs;
let template = &run.font.template;
for text_run in text_runs.iter() {
SHARED_FONT_CACHE.with(|font_cache| {
let identifier = template.identifier();
if !font_cache.borrow().contains_key(&identifier) {
let Ok(font) = run.font.font_data_and_index() else {
let identifier = &text_run.font.identifier;
if !font_cache.borrow().contains_key(identifier) {
let Some(font_data_and_index) = text_run.font.font_data_and_index() else {
return;
};
let font = font.clone().convert();
let font = font_data_and_index.convert();
font_cache.borrow_mut().insert(identifier.clone(), font);
}
let font_cache = font_cache.borrow();
let Some(font) = font_cache.get(&identifier) else {
let Some(font) = font_cache.get(identifier) else {
return;
};
@ -416,21 +409,16 @@ impl GenericDrawTarget for VelloDrawTarget {
.draw_glyphs(font)
.transform(transform)
.brush(&pattern)
.font_size(run.font.descriptor.pt_size.to_f32_px())
.font_size(text_run.pt_size)
.draw(
peniko::Fill::NonZero,
glyphs
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len()))
.map(|glyph| {
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
let x = advance + start.x + glyph_offset.x.to_f32_px();
let y = start.y + glyph_offset.y.to_f32_px();
advance += glyph.advance().to_f32_px();
vello::Glyph {
id: glyph.id(),
x,
y,
}
text_run
.glyphs_and_positions
.iter()
.map(|glyph_and_position| vello::Glyph {
id: glyph_and_position.id,
x: glyph_and_position.point.x,
y: glyph_and_position.point.y,
}),
);
});

View file

@ -8,20 +8,19 @@ use std::sync::Arc;
use canvas_traits::canvas::{
CompositionOptions, CompositionOrBlending, CompositionStyle, FillOrStrokeStyle, FillRule,
LineOptions, Path, ShadowOptions,
LineOptions, Path, ShadowOptions, TextRun,
};
use compositing_traits::SerializableImageData;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods as _};
use fonts::FontIdentifier;
use ipc_channel::ipc::IpcSharedMemory;
use kurbo::Shape;
use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
use range::Range;
use vello_cpu::{kurbo, peniko};
use webrender_api::{ImageDescriptor, ImageDescriptorFlags};
use crate::backend::{Convert, GenericDrawTarget};
use crate::canvas_data::{Filter, TextRun};
use crate::canvas_data::Filter;
thread_local! {
/// The shared font cache used by all canvases that render on a thread.
@ -282,7 +281,6 @@ impl GenericDrawTarget for VelloCPUDrawTarget {
fn fill_text(
&mut self,
text_runs: Vec<TextRun>,
start: Point2D<f32>,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f64>,
@ -291,46 +289,32 @@ impl GenericDrawTarget for VelloCPUDrawTarget {
self.ctx.set_paint(paint(style, composition_options.alpha));
self.ctx.set_transform(transform.cast().into());
self.with_composition(composition_options.composition_operation, |self_| {
let mut advance = 0.;
for run in text_runs.iter() {
let glyphs = &run.glyphs;
let template = &run.font.template;
for text_run in text_runs.iter() {
SHARED_FONT_CACHE.with(|font_cache| {
let identifier = template.identifier();
if !font_cache.borrow().contains_key(&identifier) {
let Ok(font) = run.font.font_data_and_index() else {
let identifier = &text_run.font.identifier;
if !font_cache.borrow().contains_key(identifier) {
let Some(font_data_and_index) = text_run.font.font_data_and_index() else {
return;
};
let font = font.clone().convert();
let font = font_data_and_index.convert();
font_cache.borrow_mut().insert(identifier.clone(), font);
}
let font_cache = font_cache.borrow();
let Some(font) = font_cache.get(&identifier) else {
let Some(font) = font_cache.get(identifier) else {
return;
};
self_
.ctx
.glyph_run(font)
.font_size(run.font.descriptor.pt_size.to_f32_px())
.fill_glyphs(
glyphs
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len()))
.map(|glyph| {
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
let x = advance + start.x + glyph_offset.x.to_f32_px();
let y = start.y + glyph_offset.y.to_f32_px();
advance += glyph.advance().to_f32_px();
vello_cpu::Glyph {
id: glyph.id(),
x,
y,
}
}),
);
.font_size(text_run.pt_size)
.fill_glyphs(text_run.glyphs_and_positions.iter().map(
|glyph_and_position| vello_cpu::Glyph {
id: glyph_and_position.id,
x: glyph_and_position.point.x,
y: glyph_and_position.point.y,
},
));
});
}
})