From 92c5014ec6347800f9147078ac857d090af9b76d Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 9 Aug 2017 15:36:41 -0700 Subject: [PATCH] font-renderer: Pull hinted outlines out of FreeType --- font-renderer/Cargo.toml | 7 ++ font-renderer/src/lib.rs | 86 ++++++++++++++++++++++ font-renderer/src/tests.rs | 141 +++++++++++++++++++++++++++++++++++-- 3 files changed, 228 insertions(+), 6 deletions(-) diff --git a/font-renderer/Cargo.toml b/font-renderer/Cargo.toml index 76ef30b7..b8f37586 100644 --- a/font-renderer/Cargo.toml +++ b/font-renderer/Cargo.toml @@ -6,7 +6,14 @@ authors = ["Patrick Walton "] [dependencies] app_units = "0.5" euclid = "0.15" +log = "0.3" [dependencies.freetype-sys] git = "https://github.com/pcwalton/freetype-sys.git" branch = "outline-get-cbox" + +[dependencies.pathfinder_partitioner] +path = "../partitioner" + +[dev-dependencies] +env_logger = "0.4" diff --git a/font-renderer/src/lib.rs b/font-renderer/src/lib.rs index 58288915..b40f884a 100644 --- a/font-renderer/src/lib.rs +++ b/font-renderer/src/lib.rs @@ -3,6 +3,14 @@ extern crate app_units; extern crate euclid; extern crate freetype_sys; +extern crate pathfinder_partitioner; + +#[allow(unused_imports)] +#[macro_use] +extern crate log; + +#[cfg(test)] +extern crate env_logger; use app_units::Au; use euclid::{Point2D, Size2D}; @@ -10,6 +18,7 @@ use freetype_sys::{FT_BBox, FT_Done_Face, FT_F26Dot6, FT_Face, FT_GLYPH_FORMAT_O use freetype_sys::{FT_GlyphSlot, FT_Init_FreeType, FT_Int32, FT_LOAD_TARGET_LIGHT, FT_Library}; use freetype_sys::{FT_Load_Glyph, FT_Long, FT_New_Memory_Face, FT_Outline_Get_CBox}; use freetype_sys::{FT_Set_Char_Size, FT_UInt}; +use pathfinder_partitioner::{Endpoint, Subpath}; use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::mem; @@ -24,6 +33,8 @@ mod tests; // TODO(pcwalton): Make this configurable. const GLYPH_LOAD_FLAGS: FT_Int32 = FT_LOAD_TARGET_LIGHT; +const FREETYPE_POINT_ON_CURVE: i8 = 0x01; + pub struct FontContext { library: FT_Library, faces: BTreeMap, @@ -79,6 +90,19 @@ impl FontContext { }) } + pub fn push_glyph_outline(&self, + font_instance: &FontInstanceKey, + glyph_key: &GlyphKey, + glyph_outline_buffer: &mut GlyphOutlineBuffer) + -> Result<(), ()> { + self.load_glyph(font_instance, glyph_key).ok_or(()).map(|glyph_slot| { + self.push_glyph_outline_from_glyph_slot(font_instance, + glyph_key, + glyph_slot, + glyph_outline_buffer) + }) + } + fn load_glyph(&self, font_instance: &FontInstanceKey, glyph_key: &GlyphKey) -> Option { let face = match self.faces.get(&font_instance.font_key) { @@ -145,6 +169,51 @@ impl FontContext { bounding_box } + + fn push_glyph_outline_from_glyph_slot(&self, + _: &FontInstanceKey, + _: &GlyphKey, + glyph_slot: FT_GlyphSlot, + glyph_outline_buffer: &mut GlyphOutlineBuffer) { + unsafe { + let outline = &(*glyph_slot).outline; + let mut first_point_index = 0 as u32; + let mut first_endpoint_index = glyph_outline_buffer.endpoints.len() as u32; + for contour_index in 0..outline.n_contours as usize { + let current_subpath_index = glyph_outline_buffer.subpaths.len() as u32; + let mut current_control_point_index = None; + let last_point_index = *outline.contours.offset(contour_index as isize) as u32 + 1; + for point_index in first_point_index..last_point_index { + // FIXME(pcwalton): Use `units_per_EM` to do this conversion? + // TODO(pcwalton): Approximate cubic Béziers with quadratics. + // FIXME(pcwalton): Does FreeType produce multiple consecutive off-curve points + // in a row like raw TrueType does? + let point = *outline.points.offset(point_index as isize); + let point_position = Point2D::new(point.x as f32, point.y as f32); + if (*outline.tags.offset(point_index as isize) & FREETYPE_POINT_ON_CURVE) != 0 { + glyph_outline_buffer.endpoints.push(Endpoint { + position: point_position, + control_point_index: current_control_point_index.take().unwrap_or(!0), + subpath_index: current_subpath_index, + }); + } else { + let mut control_points = &mut glyph_outline_buffer.control_points; + current_control_point_index = Some(control_points.len() as u32); + control_points.push(point_position) + } + } + + let last_endpoint_index = glyph_outline_buffer.endpoints.len() as u32; + glyph_outline_buffer.subpaths.push(Subpath { + first_endpoint_index: first_endpoint_index, + last_endpoint_index: last_endpoint_index, + }); + + first_endpoint_index = last_endpoint_index; + first_point_index = last_point_index; + } + } + } } #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] @@ -200,6 +269,23 @@ pub struct GlyphDimensions { pub advance: f32, } +pub struct GlyphOutlineBuffer { + pub endpoints: Vec, + pub control_points: Vec>, + pub subpaths: Vec, +} + +impl GlyphOutlineBuffer { + #[inline] + pub fn new() -> GlyphOutlineBuffer { + GlyphOutlineBuffer { + endpoints: vec![], + control_points: vec![], + subpaths: vec![], + } + } +} + struct Face { face: FT_Face, bytes: Vec, diff --git a/font-renderer/src/tests.rs b/font-renderer/src/tests.rs index 5788f7db..d1278187 100644 --- a/font-renderer/src/tests.rs +++ b/font-renderer/src/tests.rs @@ -1,20 +1,102 @@ // pathfinder/font-renderer/src/tests.rs use app_units::Au; +use env_logger; use euclid::Size2D; +use euclid::approxeq::ApproxEq; +use pathfinder_partitioner::Subpath; use std::fs::File; use std::io::Read; -use {FontContext, FontInstanceKey, FontKey, GlyphDimensions, GlyphKey}; +use {FontContext, FontInstanceKey, FontKey, GlyphDimensions, GlyphKey, GlyphOutlineBuffer}; static TEST_FONT_PATH: &'static str = "resources/tests/nimbus-sans/NimbusSanL-Regu.ttf"; const TEST_FONT_SIZE: Au = Au(60 * 16); -const TEST_FIRST_GLYPH_INDEX: u32 = 0x1f; +const TEST_GLYPH_ID: u32 = 68; // 'a' -// Nimbus Sans Regular 16pt., 'A' -const EXPECTED_GLYPH_ORIGIN: [i32; 2] = [1, 12]; -const EXPECTED_GLYPH_SIZE: [u32; 2] = [8, 12]; +const EXPECTED_GLYPH_ORIGIN: [i32; 2] = [0, 9]; +const EXPECTED_GLYPH_SIZE: [u32; 2] = [9, 9]; const EXPECTED_GLYPH_ADVANCE: f32 = 9.0; +static EXPECTED_GLYPH_ENDPOINTS: [[f32; 2]; 34] = [ + [ 548.0, 77.0 ], + [ 548.0, 10.0 ], + [ 490.0, 0.0 ], + [ 402.0, 82.0 ], + [ 323.0, 24.0 ], + [ 219.0, 0.0 ], + [ 89.0, 44.0 ], + [ 43.0, 158.0 ], + [ 138.0, 301.0 ], + [ 233.0, 324.0 ], + [ 310.0, 333.0 ], + [ 381.0, 353.0 ], + [ 399.0, 392.0 ], + [ 399.0, 414.0 ], + [ 362.0, 476.0 ], + [ 278.0, 494.0 ], + [ 153.0, 400.0 ], + [ 67.0, 400.0 ], + [ 104.0, 512.0 ], + [ 282.0, 576.0 ], + [ 444.0, 529.0 ], + [ 484.0, 430.0 ], + [ 484.0, 117.0 ], + [ 530.0, 75.0 ], + [ 399.0, 289.0 ], + [ 349.0, 273.0 ], + [ 261.0, 258.0 ], + [ 165.0, 228.0 ], + [ 132.0, 161.0 ], + [ 157.0, 101.0 ], + [ 238.0, 75.0 ], + [ 365.0, 124.0 ], + [ 396.0, 169.0 ], + [ 399.0, 193.0 ], +]; + +static EXPECTED_GLYPH_CONTROL_POINTS: [[f32; 2]; 29] = [ + [ 512.0, 0.0 ], + [ 410.0, 0.0 ], + [ 362.0, 43.0 ], + [ 276.0, 0.0 ], + [ 137.0, 0.0 ], + [ 43.0, 86.0 ], + [ 43.0, 262.0 ], + [ 169.0, 314.0 ], + [ 241.0, 325.0 ], + [ 365.0, 340.0 ], + [ 398.0, 366.0 ], + [ 399.0, 457.0 ], + [ 330.0, 494.0 ], + [ 163.0, 494.0 ], + [ 70.0, 474.0 ], + [ 160.0, 576.0 ], + [ 394.0, 576.0 ], + [ 484.0, 493.0 ], + [ 484.0, 75.0 ], + [ 537.0, 75.0 ], + [ 377.0, 278.0 ], + [ 325.0, 268.0 ], + [ 195.0, 249.0 ], + [ 132.0, 205.0 ], + [ 132.0, 125.0 ], + [ 183.0, 75.0 ], + [ 313.0, 75.0 ], + [ 390.0, 147.0 ], + [ 399.0, 177.0 ], +]; + +static EXPECTED_GLYPH_SUBPATHS: [Subpath; 2] = [ + Subpath { + first_endpoint_index: 0, + last_endpoint_index: 24, + }, + Subpath { + first_endpoint_index: 24, + last_endpoint_index: 34, + }, +]; + #[test] fn test_font_context_glyph_dimensions() { let mut font_context = FontContext::new(); @@ -25,7 +107,7 @@ fn test_font_context_glyph_dimensions() { font_context.add_font_from_memory(&font_key, bytes, 0).unwrap(); let font_instance = FontInstanceKey::new(&font_key, TEST_FONT_SIZE); - let glyph_key = GlyphKey::new('A' as u32 - TEST_FIRST_GLYPH_INDEX); + let glyph_key = GlyphKey::new(TEST_GLYPH_ID); let glyph_dimensions = font_context.glyph_dimensions(&font_instance, &glyph_key).unwrap(); assert_eq!(glyph_dimensions, GlyphDimensions { @@ -34,3 +116,50 @@ fn test_font_context_glyph_dimensions() { advance: EXPECTED_GLYPH_ADVANCE, }) } + +#[test] +fn test_font_context_glyph_outline() { + drop(env_logger::init()); + + let mut font_context = FontContext::new(); + + let font_key = FontKey::new(); + let mut bytes = vec![]; + File::open(TEST_FONT_PATH).unwrap().read_to_end(&mut bytes).unwrap(); + font_context.add_font_from_memory(&font_key, bytes, 0).unwrap(); + + let font_instance = FontInstanceKey::new(&font_key, TEST_FONT_SIZE); + let glyph_key = GlyphKey::new(TEST_GLYPH_ID); + let mut glyph_outline_buffer = GlyphOutlineBuffer::new(); + font_context.push_glyph_outline(&font_instance, &glyph_key, &mut glyph_outline_buffer) + .unwrap(); + + info!("endpoints: {:#?}", glyph_outline_buffer.endpoints); + info!("control points: {:#?}", glyph_outline_buffer.control_points); + + assert_eq!(glyph_outline_buffer.endpoints.len(), EXPECTED_GLYPH_ENDPOINTS.len()); + for (expected_position, endpoint) in + EXPECTED_GLYPH_ENDPOINTS.iter().zip(glyph_outline_buffer.endpoints.iter()) { + let actual_position = endpoint.position; + info!("expected endpoint: {:?} actual endpoint: {:?}", expected_position, actual_position); + assert!(expected_position[0].approx_eq(&actual_position.x) && + expected_position[1].approx_eq(&actual_position.y)); + } + + assert_eq!(glyph_outline_buffer.control_points.len(), EXPECTED_GLYPH_CONTROL_POINTS.len()); + for (expected_position, actual_position) in + EXPECTED_GLYPH_CONTROL_POINTS.iter().zip(glyph_outline_buffer.control_points.iter()) { + info!("expected control point: {:?} actual control point: {:?}", + expected_position, + actual_position); + assert!(expected_position[0].approx_eq(&actual_position.x) && + expected_position[1].approx_eq(&actual_position.y)); + } + + assert_eq!(glyph_outline_buffer.subpaths.len(), EXPECTED_GLYPH_SUBPATHS.len()); + for (expected_subpath, actual_subpath) in + EXPECTED_GLYPH_SUBPATHS.iter().zip(glyph_outline_buffer.subpaths.iter()) { + assert_eq!(expected_subpath.first_endpoint_index, actual_subpath.first_endpoint_index); + assert_eq!(expected_subpath.last_endpoint_index, actual_subpath.last_endpoint_index); + } +}