Compare commits

...

2 Commits

Author SHA1 Message Date
Nico Burns
775a292b79 Run both shapers at once
Signed-off-by: Nico Burns <nico@nicoburns.com>
2025-11-14 21:01:19 +00:00
Nico Burns
5e3358b85a Implement HarfRust shaping backend
Signed-off-by: Nico Burns <nico@nicoburns.com>
2025-11-14 21:01:19 +00:00
10 changed files with 433 additions and 8 deletions

15
Cargo.lock generated
View File

@@ -2821,6 +2821,7 @@ dependencies = [
"fontsan",
"freetype-sys",
"harfbuzz-sys",
"harfrust",
"ipc-channel",
"itertools 0.14.0",
"libc",
@@ -3785,6 +3786,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "harfrust"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"core_maths",
"read-fonts",
"smallvec",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -7129,6 +7143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358"
dependencies = [
"bytemuck",
"core_maths",
"font-types",
]

View File

@@ -78,6 +78,7 @@ gstreamer-gl-sys = "0.24"
gstreamer-sys = "0.24"
gstreamer-video = "0.24"
harfbuzz-sys = "0.6.1"
harfrust = "0.3.2"
headers = "0.4"
hitrace = "0.1.5"
html5ever = "0.36.1"

View File

@@ -14,6 +14,9 @@ test = true
doctest = false
[features]
default = ["harfrust", "harfbuzz"]
harfbuzz = ["dep:harfbuzz-sys"]
harfrust = ["dep:harfrust"]
tracing = ["dep:tracing"]
[dependencies]
@@ -25,7 +28,8 @@ euclid = { 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

@@ -881,12 +881,23 @@ impl FontFamilyDescriptor {
}
}
#[derive(PartialEq)]
pub struct FontBaseline {
pub ideographic_baseline: f32,
pub alphabetic_baseline: f32,
pub hanging_baseline: f32,
}
impl std::fmt::Debug for FontBaseline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {}",
self.ideographic_baseline, self.alphabetic_baseline, self.hanging_baseline
)
}
}
/// Given a mapping array `mapping` and a value, map that value onto
/// the value specified by the array. For instance, for FontConfig
/// values of weights, we would map these onto the CSS [0..1000] range

View File

@@ -91,10 +91,12 @@ impl FallbackFontSelectionOptions {
}
}
#[cfg(feature = "harfbuzz")]
pub(crate) fn float_to_fixed(before: usize, f: f64) -> i32 {
((1i32 << before) as f64 * f) as i32
}
#[cfg(feature = "harfbuzz")]
pub(crate) fn fixed_to_float(before: usize, f: i32) -> f64 {
f as f64 * 1.0f64 / ((1i32 << before) as f64)
}

View File

@@ -0,0 +1,113 @@
use std::io::Write as _;
use app_units::Au;
use super::harfbuzz::ShapedGlyphData as HarfBuzzShapedGlyphData;
use super::harfrust::ShapedGlyphData as HarfRustShapedGlyphData;
use super::{HarfBuzzShapedGlyphData as THarfBuzzShapedGlyphData, HarfBuzzShaper, HarfRustShaper};
use crate::{Font, FontBaseline, GlyphStore, ShapingOptions};
pub(crate) struct ShapedGlyphData {
buzz: HarfBuzzShapedGlyphData,
rust: HarfRustShapedGlyphData,
}
pub(crate) struct Shaper {
buzz: HarfBuzzShaper,
rust: HarfRustShaper,
}
impl Shaper {
pub(crate) fn new(font: &Font) -> Self {
Self {
buzz: HarfBuzzShaper::new(font),
rust: HarfRustShaper::new(font),
}
}
pub(crate) fn shaped_glyph_data(
&self,
text: &str,
options: &ShapingOptions,
) -> ShapedGlyphData {
ShapedGlyphData {
buzz: self.buzz.shaped_glyph_data(text, options),
rust: self.rust.shaped_glyph_data(text, options),
}
}
pub fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore) {
let glyph_data = self.shaped_glyph_data(text, options);
let font = self.buzz.font();
let equal = shape_data_eq(&glyph_data.buzz, &glyph_data.rust);
if !equal {
println!("SHAPING DATA DIFFERENT:");
println!("Input text:");
println!("{text}");
println!("Buzz:");
log_shape_data(&glyph_data.buzz);
println!("Rust:");
log_shape_data(&glyph_data.rust);
println!("========================");
}
super::shape_text_harfbuzz(&glyph_data.rust, font, text, options, glyphs);
}
pub fn baseline(&self) -> Option<FontBaseline> {
let buzz_baseline = self.buzz.baseline();
let rust_baseline = self.rust.baseline();
// Debug log
let equal = buzz_baseline == rust_baseline;
let eq_word = if equal { "same" } else { "diff" };
println!(
"BL ({eq_word}) C: {:?} | R: {:?}",
buzz_baseline, rust_baseline
);
rust_baseline
}
}
fn shape_data_eq(a: &impl THarfBuzzShapedGlyphData, b: &impl THarfBuzzShapedGlyphData) -> bool {
if a.len() != b.len() {
return false;
}
let mut a_y_pos = Au::new(0);
let mut b_y_pos = Au::new(0);
for i in 0..a.len() {
if a.byte_offset_of_glyph(i) != b.byte_offset_of_glyph(i) {
return false;
}
if a.entry_for_glyph(i, &mut a_y_pos) != b.entry_for_glyph(i, &mut b_y_pos) {
return false;
}
}
true
}
fn log_shape_data(data: &impl THarfBuzzShapedGlyphData) {
let mut out = std::io::stdout().lock();
writeln!(&mut out, "len: {}", data.len()).unwrap();
writeln!(&mut out, "offsets:").unwrap();
for i in 0..data.len() {
write!(&mut out, "{} ", data.byte_offset_of_glyph(i)).unwrap();
}
writeln!(&mut out).unwrap();
writeln!(&mut out, "entries:").unwrap();
let mut y_pos = Au::new(0);
for i in 0..data.len() {
let entry = data.entry_for_glyph(i, &mut y_pos);
write!(&mut out, "cp: {} ad: {} ", entry.codepoint, entry.advance.0).unwrap();
match entry.offset {
Some(offset) => write!(&mut out, "Some(x:{}, y:{})", offset.x.0, offset.y.0).unwrap(),
None => write!(&mut out, "None").unwrap(),
};
writeln!(&mut out).unwrap();
}
}

View File

@@ -209,7 +209,11 @@ impl Shaper {
}
/// Calculate the layout metrics associated with the given text with the [`Shaper`]s font.
fn shaped_glyph_data(&self, text: &str, options: &ShapingOptions) -> ShapedGlyphData {
pub(crate) fn shaped_glyph_data(
&self,
text: &str,
options: &ShapingOptions,
) -> ShapedGlyphData {
unsafe {
let hb_buffer: *mut hb_buffer_t = hb_buffer_create();
hb_buffer_set_direction(
@@ -268,7 +272,7 @@ impl Shaper {
}
}
fn font(&self) -> &Font {
pub(crate) fn font(&self) -> &Font {
unsafe { &(*self.font) }
}

View File

@@ -0,0 +1,254 @@
/* 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(crate) 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(crate) 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,
/// 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,
shaper_data,
shaper_instance,
}
}
}
impl Shaper {
pub(crate) 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)]
pub(crate) fn font(&self) -> &Font {
// SAFETY: the font actually owns this shaper so it cannot have been dropped
unsafe { &(*self.font) }
}
pub(crate) 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(crate) 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,41 @@
* 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(crate) use harfbuzz::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(crate) use harfbuzz::Shaper as HarfBuzzShaper;
#[cfg(feature = "harfrust")]
mod harfrust;
#[cfg(feature = "harfrust")]
pub(crate) use harfrust::Shaper as HarfRustShaper;
#[cfg(all(feature = "harfbuzz", feature = "harfrust"))]
mod both;
#[cfg(all(feature = "harfbuzz", feature = "harfrust"))]
pub(crate) use BothShaper as Shaper;
// Configure default shaper (actually used)
#[cfg(all(feature = "harfbuzz", not(feature = "harfrust")))]
pub(crate) use HarfBuzzShaper as Shaper;
#[cfg(all(not(feature = "harfbuzz"), feature = "harfrust"))]
pub(crate) use HarfRustShaper as Shaper;
#[cfg(all(feature = "harfbuzz", feature = "harfrust"))]
pub(crate) use both::Shaper as BothShaper;
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 {
@@ -31,6 +51,7 @@ fn unicode_script_to_iso15924_tag(script: unicode_script::Script) -> u32 {
u32::from_be_bytes(bytes)
}
#[derive(PartialEq)]
struct ShapedGlyphEntry {
codepoint: GlyphId,
advance: Au,

View File

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