Compare commits

...

1 Commits

Author SHA1 Message Date
Nico Burns
bfd90c9f7d Implement HarfRust shaping backend
Signed-off-by: Nico Burns <nico@nicoburns.com>
2025-08-22 20:00:18 +01:00
14 changed files with 313 additions and 18 deletions

29
Cargo.lock generated
View File

@@ -2684,6 +2684,7 @@ dependencies = [
"fontsan",
"freetype-sys",
"harfbuzz-sys",
"harfrust",
"ipc-channel",
"itertools 0.14.0",
"libc",
@@ -2695,7 +2696,7 @@ dependencies = [
"parking_lot",
"profile_traits",
"range",
"read-fonts",
"read-fonts 0.29.3",
"serde",
"servo-tracing",
"servo_allocator",
@@ -3658,6 +3659,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "harfrust"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca4253de099ee464aea026833ee8f3968c08bfe0065bfb4f47c6ac590d533590"
dependencies = [
"bitflags 2.9.2",
"bytemuck",
"core_maths",
"read-fonts 0.33.1",
"smallvec",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -6882,6 +6896,17 @@ dependencies = [
"font-types",
]
[[package]]
name = "read-fonts"
version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50ea612a55c08586a1d15134be8a776186c440c312ebda3b9e8efbfe4255b7f4"
dependencies = [
"bytemuck",
"core_maths",
"font-types",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -8029,7 +8054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607"
dependencies = [
"bytemuck",
"read-fonts",
"read-fonts 0.29.3",
]
[[package]]

View File

@@ -75,6 +75,7 @@ gstreamer-gl-sys = "0.23"
gstreamer-sys = "0.23"
gstreamer-video = "0.23"
harfbuzz-sys = "0.6.1"
harfrust = "0.1.1"
headers = "0.4"
hitrace = "0.1.5"
html5ever = "0.35"

View File

@@ -26,7 +26,7 @@ crossbeam-channel = { workspace = true }
cssparser = { workspace = true }
euclid = { workspace = true }
font-kit = { version = "0.14", optional = true }
fonts = { path = "../fonts" }
fonts = { path = "../fonts", default-features = false }
ipc-channel = { workspace = true }
kurbo = { workspace = true }
log = { workspace = true }

View File

@@ -38,7 +38,7 @@ crossbeam-channel = { workspace = true }
devtools_traits = { workspace = true }
embedder_traits = { workspace = true }
euclid = { workspace = true }
fonts = { path = "../fonts" }
fonts = { path = "../fonts", default-features = false }
ipc-channel = { workspace = true }
keyboard-types = { workspace = true }
layout_api = { workspace = true }

View File

@@ -14,6 +14,9 @@ test = true
doctest = false
[features]
default = ["harfbuzz"]
harfbuzz = ["dep:harfbuzz-sys"]
harfrust = ["dep:harfrust"]
tracing = ["dep:tracing"]
[dependencies]
@@ -27,7 +30,8 @@ fnv = { workspace = true }
fonts_traits = { workspace = true }
fontsan = { git = "https://github.com/servo/fontsan" }
# FIXME (#34517): macOS only needs this when building libservo without `--features media-gstreamer`
harfbuzz-sys = { workspace = true, features = ["bundled"] }
harfbuzz-sys = { workspace = true, optional = true, features = ["bundled"] }
harfrust = { workspace = true, optional = true }
ipc-channel = { workspace = true }
itertools = { workspace = true }
libc = { workspace = true }

View File

@@ -82,6 +82,7 @@ impl FallbackFontSelectionOptions {
}
}
#[cfg(feature = "harfbuzz")]
pub(crate) fn float_to_fixed(before: usize, f: f64) -> i32 {
((1i32 << before) as f64 * f) as i32
}

View File

@@ -0,0 +1,252 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 app_units::Au;
use euclid::default::Point2D;
use harfrust::{
Feature, FontRef as HarfRustFontRef, GlyphBuffer, Script, ShaperData, ShaperInstance, Tag,
UnicodeBuffer, Variation,
};
use num_traits::Zero as _;
use read_fonts::TableProvider;
use read_fonts::types::BigEndian;
use smallvec::SmallVec;
use super::{HarfBuzzShapedGlyphData, ShapedGlyphEntry, unicode_script_to_iso15924_tag};
use crate::{Font, FontBaseline, FontData, GlyphStore, ShapingFlags, ShapingOptions};
/// Convert a `webrender_api::FontVariation` to a `harfrust::Variation`
fn wr_variation_to_hr_varation(wr_variation: webrender_api::FontVariation) -> harfrust::Variation {
Variation {
tag: Tag::from_u32(wr_variation.tag),
value: wr_variation.value,
}
}
pub struct ShapedGlyphData {
data: GlyphBuffer,
scale: f64,
}
impl HarfBuzzShapedGlyphData for ShapedGlyphData {
fn len(&self) -> usize {
self.data.len()
}
fn byte_offset_of_glyph(&self, i: usize) -> u32 {
self.data.glyph_infos()[i].cluster
}
fn entry_for_glyph(&self, i: usize, y_pos: &mut app_units::Au) -> super::ShapedGlyphEntry {
let glyph_info_i = self.data.glyph_infos()[i];
let pos_info_i = self.data.glyph_positions()[i];
let x_offset = Au::from_f64_px(pos_info_i.x_offset as f64 * self.scale);
let y_offset = Au::from_f64_px(pos_info_i.y_offset as f64 * self.scale);
let x_advance = Au::from_f64_px(pos_info_i.x_advance as f64 * self.scale);
let y_advance = Au::from_f64_px(pos_info_i.y_advance as f64 * self.scale);
let offset = if x_offset.is_zero() && y_offset.is_zero() && y_advance.is_zero() {
None
} else {
// adjust the pen..
if y_advance > Au::zero() {
*y_pos -= y_advance;
}
Some(Point2D::new(x_offset, *y_pos - y_offset))
};
ShapedGlyphEntry {
codepoint: glyph_info_i.glyph_id,
advance: x_advance,
offset,
}
}
}
pub struct Shaper {
font: *const Font,
/// The raw byte data of the font
font_data: FontData,
/// The index of a font in it's collection (.ttc)
/// If the font file is not a collection then this is 0
font_index: u32,
// Used for scaling HarfRust's output
scale: f64,
ppem: f64,
/// Pre-computed data for shaping a font
shaper_data: ShaperData,
/// Pre-computed data for shaping a variable font with a particular set of variations.
/// If there are no variations then we don't create a ShaperInstance.
shaper_instance: Option<ShaperInstance>,
}
// `Font` and `FontData` are both threadsafe, so we can make the data structures here as thread-safe as well.
#[allow(unsafe_code)]
unsafe impl Sync for Shaper {}
#[allow(unsafe_code)]
unsafe impl Send for Shaper {}
impl Shaper {
pub(crate) fn new(font: &Font) -> Self {
let raw_font = font
.font_data_and_index()
.expect("Error creating shaper for font");
let font_data = raw_font.data.clone();
let font_index = raw_font.index;
// Set points-per-em. if zero, performs no hinting in that direction
let ppem = font.descriptor.pt_size.to_f64_px();
let font_ref = read_fonts::FontRef::from_index(font_data.as_ref(), font_index).unwrap();
let units_per_em = font_ref.head().unwrap().units_per_em();
let scale = ppem / (units_per_em as f64);
// Create cached shaping data for the font
let hr_font = HarfRustFontRef::from_index(font_data.as_ref(), font_index).unwrap();
let shaper_data = ShaperData::new(&hr_font);
// If variable fonts are enabled and the font has variations, create a ShaperInstance to set on the shaper.
let variations = font.variations();
let shaper_instance =
if servo_config::pref!(layout_variable_fonts_enabled) && !variations.is_empty() {
let variations_iter = variations.iter().copied().map(wr_variation_to_hr_varation);
Some(ShaperInstance::from_variations(&hr_font, variations_iter))
} else {
None
};
Self {
font: font as *const Font,
font_data,
font_index,
scale,
ppem,
shaper_data,
shaper_instance,
}
}
}
impl Shaper {
fn shaped_glyph_data(&self, text: &str, options: &crate::ShapingOptions) -> ShapedGlyphData {
let mut buffer = UnicodeBuffer::new();
// Set direction
buffer.set_direction(if options.flags.contains(ShapingFlags::RTL_FLAG) {
harfrust::Direction::RightToLeft
} else {
harfrust::Direction::LeftToRight
});
// Set script
let script_tag = Tag::from_u32(unicode_script_to_iso15924_tag(options.script));
let script = Script::from_iso15924_tag(script_tag).unwrap();
buffer.set_script(script);
// Push text
buffer.push_str(text);
// Features
let mut features = SmallVec::<[Feature; 2]>::new();
if options
.flags
.contains(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG)
{
features.push(Feature::new(Tag::new(b"liga"), 0, ..));
}
if options
.flags
.contains(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
{
features.push(Feature::new(Tag::new(b"kern"), 0, ..));
}
let hr_font =
HarfRustFontRef::from_index(self.font_data.as_ref(), self.font_index).unwrap();
let shaper = self
.shaper_data
.shaper(&hr_font)
.instance(self.shaper_instance.as_ref())
.build();
let glyph_buffer = shaper.shape(buffer, &features);
ShapedGlyphData {
data: glyph_buffer,
scale: self.scale,
}
}
#[allow(unsafe_code)]
fn font(&self) -> &Font {
// SAFETY: the font actually owns this shaper so it cannot have been dropped
unsafe { &(*self.font) }
}
pub fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore) {
let glyph_data = self.shaped_glyph_data(text, options);
let font = self.font();
super::shape_text_harfbuzz(&glyph_data, font, text, options, glyphs);
}
pub fn baseline(&self) -> Option<crate::FontBaseline> {
let font_ref =
read_fonts::FontRef::from_index(self.font_data.as_ref(), self.font_index).unwrap();
// Load the horizontal axis of the BASE table
let base_table = font_ref.base().ok()?;
let horiz_axis = base_table.horiz_axis()?.ok()?;
// Get the index of each baseline tag in the tag list
let tag_list = horiz_axis.base_tag_list()?.ok()?;
let baseline_tags = tag_list.baseline_tags();
let romn_tag_idx = baseline_tags
.binary_search(&BigEndian::from(Tag::new(b"romn")))
.ok();
let hang_tag_idx = baseline_tags
.binary_search(&BigEndian::from(Tag::new(b"hang")))
.ok();
let ideo_tag_idx = baseline_tags
.binary_search(&BigEndian::from(Tag::new(b"ideo")))
.ok();
// Bail early if none of the baseline tags exist in the tag list
if romn_tag_idx.is_none() && hang_tag_idx.is_none() && ideo_tag_idx.is_none() {
return None;
}
// Find the DFLT (default) script record
let script_list = horiz_axis.base_script_list().ok()?;
let script_records = script_list.base_script_records();
let default_script_record_idx = script_records
.binary_search_by_key(&Tag::from_be_bytes(*b"DFLT"), |record| {
record.base_script_tag()
})
.ok()?;
let default_script_record = &script_records[default_script_record_idx];
// Lookup the baseline coordinates DFLT script record
let base_script = default_script_record
.base_script(script_list.offset_data())
.ok()?;
let base_script_values = base_script.base_values()?.ok()?;
let base_coords = base_script_values.base_coords();
// Search for the baseline corresponding to a given baseline index
let get_coord = |idx: usize| -> Option<f32> {
base_coords
.get(idx)
.ok()
.map(|coord| coord.coordinate() as f32 * self.scale as f32)
};
Some(FontBaseline {
ideographic_baseline: ideo_tag_idx.and_then(get_coord).unwrap_or(0.0),
alphabetic_baseline: romn_tag_idx.and_then(get_coord).unwrap_or(0.0),
hanging_baseline: hang_tag_idx.and_then(get_coord).unwrap_or(0.0),
})
}
}

View File

@@ -2,21 +2,29 @@
* 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/. */
mod harfbuzz;
use std::cmp;
use app_units::Au;
use base::text::is_bidi_control;
use euclid::default::Point2D;
use fonts_traits::ByteIndex;
pub use harfbuzz::{ShapedGlyphData, Shaper};
use log::debug;
use num_traits::Zero as _;
const NO_GLYPH: i32 = -1;
use crate::{Font, GlyphData, GlyphId, GlyphStore, ShapingOptions, advance_for_shaped_glyph};
#[cfg(feature = "harfbuzz")]
mod harfbuzz;
#[cfg(feature = "harfbuzz")]
pub use harfbuzz::Shaper;
#[cfg(feature = "harfrust")]
mod harfrust;
#[cfg(all(feature = "harfrust", not(feature = "harfbuzz")))]
pub use harfrust::Shaper;
const NO_GLYPH: i32 = -1;
/// Utility function to convert a `unicode_script::Script` enum into the corresponding `c_uint` tag that
/// harfbuzz uses to represent unicode scipts.
fn unicode_script_to_iso15924_tag(script: unicode_script::Script) -> u32 {

View File

@@ -27,7 +27,7 @@ data-url = { workspace = true }
embedder_traits = { workspace = true }
euclid = { workspace = true }
fnv = { workspace = true }
fonts = { path = "../fonts" }
fonts = { path = "../fonts", default-features = false }
fonts_traits = { workspace = true }
fxhash = { workspace = true }
html5ever = { workspace = true }

View File

@@ -66,7 +66,7 @@ embedder_traits = { workspace = true }
encoding_rs = { workspace = true }
euclid = { workspace = true }
fnv = { workspace = true }
fonts = { path = "../fonts" }
fonts = { path = "../fonts", default-features = false }
fonts_traits = { workspace = true }
fxhash = { workspace = true }
glow = { workspace = true }

View File

@@ -21,7 +21,9 @@ bluetooth = [
"script/bluetooth",
"script_traits/bluetooth",
]
default = ["clipboard", "raqote"]
default = ["clipboard", "raqote", "harfbuzz"]
harfbuzz = ["fonts/harfbuzz"]
harfrust = ["fonts/harfrust"]
clipboard = ["dep:arboard"]
crown = ["script/crown"]
debugmozjs = ["script/debugmozjs"]
@@ -84,7 +86,7 @@ dpi = { workspace = true }
embedder_traits = { workspace = true }
env_logger = { workspace = true }
euclid = { workspace = true }
fonts = { path = "../fonts" }
fonts = { path = "../fonts", default-features = false }
gleam = { workspace = true }
gstreamer = { workspace = true, optional = true }
ipc-channel = { workspace = true }

View File

@@ -54,7 +54,7 @@ pub struct FontDataAndIndex {
pub index: u32,
}
#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum FontDataError {
FailedToLoad,
}

View File

@@ -21,7 +21,7 @@ constellation_traits = { workspace = true }
embedder_traits = { workspace = true }
euclid = { workspace = true }
fnv = { workspace = true }
fonts = { path = "../../fonts" }
fonts = { path = "../../fonts", default-features = false }
fonts_traits = { workspace = true }
fxhash = { workspace = true }
html5ever = { workspace = true }

View File

@@ -37,7 +37,7 @@ ProductName = "Servo"
[features]
crown = ["libservo/crown"]
debugmozjs = ["libservo/debugmozjs"]
default = ["max_log_level", "webgpu", "webxr"]
default = ["max_log_level", "webgpu", "webxr", "libservo/default"]
jitspew = ["libservo/jitspew"]
js_backtrace = ["libservo/js_backtrace"]
max_log_level = ["log/release_max_level_info"]
@@ -54,6 +54,8 @@ webxr = ["libservo/webxr"]
vello = ["libservo/vello"]
vello_cpu = ["libservo/vello_cpu"]
raqote = ["libservo/raqote"]
harfbuzz = ["libservo/harfbuzz"]
harfrust = ["libservo/harfrust"]
[dependencies]
cfg-if = { workspace = true }
@@ -68,7 +70,7 @@ image = { workspace = true }
ipc-channel = { workspace = true }
keyboard-types = { workspace = true }
libc = { workspace = true }
libservo = { path = "../../components/servo", features = ["background_hang_monitor", "bluetooth", "testbinding"] }
libservo = { path = "../../components/servo", default-features = false, features = ["background_hang_monitor", "bluetooth", "testbinding"] }
log = { workspace = true }
mime_guess = { workspace = true }
raw-window-handle = { workspace = true }
@@ -130,7 +132,7 @@ winit = { workspace = true }
sig = "1.0"
[target.'cfg(target_os = "windows")'.dependencies]
libservo = { path = "../../components/servo", features = ["no-wgl"] }
libservo = { path = "../../components/servo", default-features = false, features = ["no-wgl"] }
windows-sys = { workspace = true, features = ["Win32_Graphics_Gdi"] }
[target.'cfg(target_os = "macos")'.dependencies]