gfx: Implement letter-spacing per CSS 2.1 § 16.4.

The ligature disabling code has been manually verified, but I was unable
to reftest it. (The only way I could think of would be to create an
Ahem-like font with a ligature table, but that would be an awful lot of
work.)

Near as I can tell, the method used to apply the spacing (manually
inserting extra advance post-shaping) matches Gecko.
This commit is contained in:
Patrick Walton 2014-12-10 14:00:12 -08:00
parent 758d79fff4
commit 07bc97e3e2
11 changed files with 246 additions and 47 deletions

View file

@ -4,7 +4,8 @@
extern crate harfbuzz;
use font::{Font, FontHandleMethods, FontTableMethods, FontTableTag};
use font::{Font, FontHandleMethods, FontTableMethods, FontTableTag, IGNORE_LIGATURES_SHAPING_FLAG};
use font::{ShapingOptions};
use platform::font::FontTable;
use text::glyph::{CharIndex, GlyphStore, GlyphId, GlyphData};
use text::shaping::ShaperMethods;
@ -18,9 +19,11 @@ use harfbuzz::{hb_bool_t};
use harfbuzz::{hb_buffer_add_utf8};
use harfbuzz::{hb_buffer_destroy};
use harfbuzz::{hb_buffer_get_glyph_positions};
use harfbuzz::{hb_buffer_get_length};
use harfbuzz::{hb_buffer_set_direction};
use harfbuzz::{hb_face_destroy};
use harfbuzz::{hb_face_t, hb_font_t};
use harfbuzz::{hb_feature_t};
use harfbuzz::{hb_font_create};
use harfbuzz::{hb_font_destroy, hb_buffer_create};
use harfbuzz::{hb_font_funcs_create};
@ -47,6 +50,9 @@ use std::ptr;
static NO_GLYPH: i32 = -1;
static CONTINUATION_BYTE: i32 = -2;
static LIGA: u32 = ((b'l' as u32) << 24) | ((b'i' as u32) << 16) | ((b'g' as u32) << 8) |
(b'a' as u32);
pub struct ShapedGlyphData {
count: int,
glyph_infos: *mut hb_glyph_info_t,
@ -131,10 +137,16 @@ impl ShapedGlyphData {
}
}
struct FontAndShapingOptions {
font: *mut Font,
options: ShapingOptions,
}
pub struct Shaper {
hb_face: *mut hb_face_t,
hb_font: *mut hb_font_t,
hb_funcs: *mut hb_font_funcs_t,
font_and_shaping_options: Box<FontAndShapingOptions>,
}
#[unsafe_destructor]
@ -154,13 +166,18 @@ impl Drop for Shaper {
}
impl Shaper {
pub fn new(font: &mut Font) -> Shaper {
pub fn new(font: &mut Font, options: &ShapingOptions) -> Shaper {
unsafe {
// Indirection for Rust Issue #6248, dynamic freeze scope artificially extended
let font_ptr = font as *mut Font;
let hb_face: *mut hb_face_t = hb_face_create_for_tables(get_font_table_func,
font_ptr as *mut c_void,
None);
let mut font_and_shaping_options = box FontAndShapingOptions {
font: font,
options: *options,
};
let hb_face: *mut hb_face_t =
hb_face_create_for_tables(get_font_table_func,
(&mut *font_and_shaping_options)
as *mut FontAndShapingOptions
as *mut c_void,
None);
let hb_font: *mut hb_font_t = hb_font_create(hb_face);
// Set points-per-em. if zero, performs no hinting in that direction.
@ -178,16 +195,21 @@ impl Shaper {
hb_font_funcs_set_glyph_func(hb_funcs, glyph_func, ptr::null_mut(), None);
hb_font_funcs_set_glyph_h_advance_func(hb_funcs, glyph_h_advance_func, ptr::null_mut(), None);
hb_font_funcs_set_glyph_h_kerning_func(hb_funcs, glyph_h_kerning_func, ptr::null_mut(), ptr::null_mut());
hb_font_set_funcs(hb_font, hb_funcs, font_ptr as *mut c_void, None);
hb_font_set_funcs(hb_font, hb_funcs, font as *mut Font as *mut c_void, None);
Shaper {
hb_face: hb_face,
hb_font: hb_font,
hb_funcs: hb_funcs,
font_and_shaping_options: font_and_shaping_options,
}
}
}
pub fn set_options(&mut self, options: &ShapingOptions) {
self.font_and_shaping_options.options = *options
}
fn float_to_fixed(f: f64) -> i32 {
float_to_fixed(16, f)
}
@ -200,7 +222,7 @@ impl Shaper {
impl ShaperMethods for Shaper {
/// Calculate the layout metrics associated with the given text when painted in a specific
/// font.
fn shape_text(&self, text: &str, glyphs: &mut GlyphStore) {
fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore) {
unsafe {
let hb_buffer: *mut hb_buffer_t = hb_buffer_create();
hb_buffer_set_direction(hb_buffer, HB_DIRECTION_LTR);
@ -211,15 +233,29 @@ impl ShaperMethods for Shaper {
0,
text.len() as c_int);
hb_shape(self.hb_font, hb_buffer, ptr::null_mut(), 0);
self.save_glyph_results(text, glyphs, hb_buffer);
let mut features = Vec::new();
if options.flags.contains(IGNORE_LIGATURES_SHAPING_FLAG) {
features.push(hb_feature_t {
_tag: LIGA,
_value: 0,
_start: 0,
_end: hb_buffer_get_length(hb_buffer),
})
}
hb_shape(self.hb_font, hb_buffer, features.as_mut_ptr(), features.len() as u32);
self.save_glyph_results(text, options, glyphs, hb_buffer);
hb_buffer_destroy(hb_buffer);
}
}
}
impl Shaper {
fn save_glyph_results(&self, text: &str, glyphs: &mut GlyphStore, buffer: *mut hb_buffer_t) {
fn save_glyph_results(&self,
text: &str,
options: &ShapingOptions,
glyphs: &mut GlyphStore,
buffer: *mut hb_buffer_t) {
let glyph_data = ShapedGlyphData::new(buffer);
let glyph_count = glyph_data.len();
let byte_max = text.len() as int;
@ -401,8 +437,9 @@ impl Shaper {
// (i.e., pretend there are no combining character sequences).
// 1-to-1 mapping of character to glyph also treated as ligature start.
let shape = glyph_data.get_entry_for_glyph(glyph_span.begin(), &mut y_pos);
let advance = self.advance_for_shaped_glyph(shape.advance, options);
let data = GlyphData::new(shape.codepoint,
shape.advance,
advance,
shape.offset,
false,
true,
@ -450,6 +487,13 @@ impl Shaper {
// lookup table for finding detailed glyphs by associated char index.
glyphs.finalize_changes();
}
fn advance_for_shaped_glyph(&self, advance: Au, options: &ShapingOptions) -> Au {
match options.letter_spacing {
None => advance,
Some(spacing) => advance + spacing,
}
}
}
/// Callbacks from Harfbuzz when font map and glyph advance lookup needed.
@ -504,13 +548,19 @@ extern fn glyph_h_kerning_func(_: *mut hb_font_t,
}
// Callback to get a font table out of a font.
extern fn get_font_table_func(_: *mut hb_face_t, tag: hb_tag_t, user_data: *mut c_void) -> *mut hb_blob_t {
extern fn get_font_table_func(_: *mut hb_face_t,
tag: hb_tag_t,
user_data: *mut c_void)
-> *mut hb_blob_t {
unsafe {
let font: *const Font = user_data as *const Font;
assert!(font.is_not_null());
// NB: These asserts have security implications.
let font_and_shaping_options: *const FontAndShapingOptions =
user_data as *const FontAndShapingOptions;
assert!(font_and_shaping_options.is_not_null());
assert!((*font_and_shaping_options).font.is_not_null());
// TODO(Issue #197): reuse font table data, which will change the unsound trickery here.
match (*font).get_table_for_tag(tag as FontTableTag) {
match (*(*font_and_shaping_options).font).get_table_for_tag(tag as FontTableTag) {
None => ptr::null_mut(),
Some(ref font_table) => {
let skinny_font_table_ptr: *const FontTable = font_table; // private context

View file

@ -7,6 +7,7 @@
//!
//! Currently, only harfbuzz bindings are implemented.
use font::ShapingOptions;
use text::glyph::GlyphStore;
pub use text::shaping::harfbuzz::Shaper;
@ -14,6 +15,6 @@ pub use text::shaping::harfbuzz::Shaper;
pub mod harfbuzz;
pub trait ShaperMethods {
fn shape_text(&self, text: &str, glyphs: &mut GlyphStore);
fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore);
}

View file

@ -2,15 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use font::{Font, RunMetrics, FontMetrics};
use font::{Font, FontHandleMethods, FontMetrics, IS_WHITESPACE_SHAPING_FLAG, RunMetrics};
use font::{ShapingOptions};
use platform::font_template::FontTemplateData;
use servo_util::geometry::Au;
use servo_util::range::Range;
use servo_util::vec::{Comparator, FullBinarySearchMethods};
use std::slice::Items;
use sync::Arc;
use text::glyph::{CharIndex, GlyphStore};
use font::FontHandleMethods;
use platform::font_template::FontTemplateData;
/// A single "paragraph" of text in one font size and style.
#[deriving(Clone)]
@ -117,8 +117,8 @@ impl<'a> Iterator<Range<CharIndex>> for LineIterator<'a> {
}
impl<'a> TextRun {
pub fn new(font: &mut Font, text: String) -> TextRun {
let glyphs = TextRun::break_and_shape(font, text.as_slice());
pub fn new(font: &mut Font, text: String, options: &ShapingOptions) -> TextRun {
let glyphs = TextRun::break_and_shape(font, text.as_slice(), options);
let run = TextRun {
text: Arc::new(text),
font_metrics: font.metrics.clone(),
@ -129,7 +129,8 @@ impl<'a> TextRun {
return run;
}
pub fn break_and_shape(font: &mut Font, text: &str) -> Vec<GlyphRun> {
pub fn break_and_shape(font: &mut Font, text: &str, options: &ShapingOptions)
-> Vec<GlyphRun> {
// TODO(Issue #230): do a better job. See Gecko's LineBreaker.
let mut glyphs = vec!();
let (mut byte_i, mut char_i) = (0u, CharIndex(0));
@ -165,8 +166,14 @@ impl<'a> TextRun {
let slice = text.slice(byte_last_boundary, byte_i);
debug!("creating glyph store for slice {} (ws? {}), {} - {} in run {}",
slice, !cur_slice_is_whitespace, byte_last_boundary, byte_i, text);
let mut options = *options;
if !cur_slice_is_whitespace {
options.flags.insert(IS_WHITESPACE_SHAPING_FLAG);
}
glyphs.push(GlyphRun {
glyph_store: font.shape_text(slice, !cur_slice_is_whitespace),
glyph_store: font.shape_text(slice, &options),
range: Range::new(char_last_boundary, char_i - char_last_boundary),
});
byte_last_boundary = byte_i;
@ -182,8 +189,14 @@ impl<'a> TextRun {
let slice = text.slice_from(byte_last_boundary);
debug!("creating glyph store for final slice {} (ws? {}), {} - {} in run {}",
slice, cur_slice_is_whitespace, byte_last_boundary, text.len(), text);
let mut options = *options;
if cur_slice_is_whitespace {
options.flags.insert(IS_WHITESPACE_SHAPING_FLAG);
}
glyphs.push(GlyphRun {
glyph_store: font.shape_text(slice, cur_slice_is_whitespace),
glyph_store: font.shape_text(slice, &options),
range: Range::new(char_last_boundary, char_i - char_last_boundary),
});
}