From 02bab4fcc781744505fe90cd0d01cfac8d9afe96 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Mon, 15 Jan 2018 17:32:31 -0800 Subject: [PATCH] Add fontconfig support to locate fonts Also restructure things a little bit to facilitate more robust fallback implementation. --- .gitignore | 1 + Cargo.toml | 2 +- font/src/ft/fcwrap.rs | 266 ++++++++++++++++++++++++++++++++++++++++ font/src/ft/ftwrap.rs | 8 +- font/src/ft/mod.rs | 1 + src/main.rs | 277 +++++++++++++++++++++++++++++++----------- 6 files changed, 479 insertions(+), 76 deletions(-) create mode 100644 font/src/ft/fcwrap.rs diff --git a/.gitignore b/.gitignore index cd6768fb1..6e32e06f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /Cargo.lock /target/ **/*.rs.bk +.*.sw* diff --git a/Cargo.toml b/Cargo.toml index 40235295c..b9a63b40a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" [dependencies] failure = "0.1.1" hexdump = "0.1.0" -sdl2 = {version="0.31.0", features=["bundled"]} +sdl2 = {version="0.31.0", features=["bundled", "static-link"]} vte = "0.3.2" [dependencies.font] diff --git a/font/src/ft/fcwrap.rs b/font/src/ft/fcwrap.rs new file mode 100644 index 000000000..1ab67febf --- /dev/null +++ b/font/src/ft/fcwrap.rs @@ -0,0 +1,266 @@ +//! Slightly higher level helper for fontconfig +use failure::{self, Error}; +pub use fontconfig::fontconfig::*; +use std::ffi::{CStr, CString}; +use std::mem; +use std::ptr; + +static FC_MONO: i32 = 100; + +pub struct FontSet { + fonts: *mut FcFontSet, +} + +impl Drop for FontSet { + fn drop(&mut self) { + unsafe { + FcFontSetDestroy(self.fonts); + } + } +} + +pub struct FontSetIter<'a> { + set: &'a FontSet, + position: isize, +} + +impl<'a> Iterator for FontSetIter<'a> { + type Item = Pattern; + + fn next(&mut self) -> Option { + unsafe { + if self.position == (*self.set.fonts).nfont as isize { + None + } else { + let pat = *(*self.set.fonts) + .fonts + .offset(self.position) + .as_mut() + .unwrap(); + FcPatternReference(pat); + self.position += 1; + Some(Pattern { pat }) + } + } + } +} + +impl FontSet { + pub fn iter(&self) -> FontSetIter { + FontSetIter { + set: &self, + position: 0, + } + } + + pub fn add(&mut self, pat: &Pattern) { + unsafe { + FcFontSetAdd(self.fonts, pat.pat); + } + } +} + +#[repr(C)] +pub enum MatchKind { + Pattern = FcMatchPattern as isize, + Font = FcMatchFont as isize, + Scan = FcMatchScan as isize, +} + +pub struct FcResultWrap(FcResult); + +impl FcResultWrap { + pub fn succeeded(&self) -> bool { + self.0 == FcResultMatch + } + + pub fn as_err(&self) -> Error { + // the compiler thinks we defined these globals, when all + // we did was import them from elsewhere + #[allow(non_upper_case_globals)] + match self.0 { + FcResultMatch => failure::err_msg("FcResultMatch"), + FcResultNoMatch => failure::err_msg("FcResultNoMatch"), + FcResultTypeMismatch => failure::err_msg("FcResultTypeMismatch"), + FcResultNoId => failure::err_msg("FcResultNoId"), + FcResultOutOfMemory => failure::err_msg("FcResultOutOfMemory"), + _ => format_err!("FcResult holds invalid value {}", self.0), + } + } + + pub fn result(&self, t: T) -> Result { + match self.0 { + FcResultMatch => Ok(t), + _ => Err(self.as_err()), + } + } +} + +pub struct Pattern { + pat: *mut FcPattern, +} + +impl Pattern { + pub fn new() -> Result { + unsafe { + let p = FcPatternCreate(); + ensure!(!p.is_null(), "FcPatternCreate failed"); + Ok(Pattern { pat: p }) + } + } + + pub fn add_string(&mut self, key: &str, value: &str) -> Result<(), Error> { + let key = CString::new(key)?; + let value = CString::new(value)?; + unsafe { + ensure!( + FcPatternAddString( + self.pat, + key.as_ptr(), + value.as_ptr() as *const u8, + ) != 0, + "failed to add string property {:?} -> {:?}", + key, + value + ); + Ok(()) + } + } + + pub fn add_integer(&mut self, key: &str, value: i32) -> Result<(), Error> { + let key = CString::new(key)?; + unsafe { + ensure!( + FcPatternAddInteger(self.pat, key.as_ptr(), value) != 0, + "failed to set integer property {:?} -> {}", + key, + value + ); + Ok(()) + } + } + + + pub fn family(&mut self, family: &str) -> Result<(), Error> { + self.add_string("family", family) + } + + pub fn monospace(&mut self) -> Result<(), Error> { + self.add_integer("spacing", FC_MONO) + } + + pub fn print(&self) { + unsafe { + FcPatternPrint(self.pat); + } + } + + pub fn format(&self, fmt: &str) -> Result { + let fmt = CString::new(fmt)?; + unsafe { + let s = FcPatternFormat(self.pat, fmt.as_ptr() as *const u8); + ensure!(!s.is_null(), "failed to format pattern"); + + let res = CStr::from_ptr(s as *const i8).to_string_lossy().into_owned(); + FcStrFree(s); + Ok(res) + } + } + + pub fn render_prepare(&self, pat: &Pattern) -> Result { + unsafe { + let pat = FcFontRenderPrepare(ptr::null_mut(), self.pat, pat.pat); + ensure!(!pat.is_null(), "failed to prepare pattern"); + Ok(Pattern { pat }) + } + } + + pub fn parse(pattern: &str) -> Result { + let pattern = CString::new(pattern)?; + unsafe { + let p = FcNameParse(pattern.as_ptr() as *const u8); + ensure!(!p.is_null(), "failed to parse {:?}", pattern); + Ok(Pattern { pat: p }) + } + } + + pub fn config_substitute( + &mut self, + match_kind: MatchKind, + ) -> Result<(), Error> { + unsafe { + ensure!( + FcConfigSubstitute( + ptr::null_mut(), + self.pat, + mem::transmute(match_kind), + ) != 0, + "FcConfigSubstitute failed" + ); + Ok(()) + } + } + + pub fn default_substitute(&mut self) { + unsafe { + FcDefaultSubstitute(self.pat); + } + } + + pub fn find_match(&self) -> Result { + unsafe { + let mut res = FcResultWrap(0); + let pat = FcFontMatch( + ptr::null_mut(), + self.pat, + &mut res.0 as *mut _, + ); + res.result(Pattern{pat}) + } + } + + pub fn sort(&self, trim: bool) -> Result { + unsafe { + let mut res = FcResultWrap(0); + let fonts = FcFontSort( + ptr::null_mut(), + self.pat, + if trim { 1 } else { 0 }, + ptr::null_mut(), + &mut res.0 as *mut _, + ); + + res.result(FontSet{fonts}) + } + } + + pub fn get_file(&self) -> Result { + self.get_string("file") + } + + pub fn get_string(&self, key: &str) -> Result { + unsafe { + let key = CString::new(key)?; + let mut ptr: *mut u8 = ptr::null_mut(); + let res = FcResultWrap(FcPatternGetString( + self.pat, + key.as_ptr(), + 0, + &mut ptr as *mut *mut u8, + )); + if !res.succeeded() { + Err(res.as_err()) + } else { + Ok(CStr::from_ptr(ptr as *const i8).to_string_lossy().into_owned()) + } + } + } +} + +impl Drop for Pattern { + fn drop(&mut self) { + unsafe { + FcPatternDestroy(self.pat); + } + } +} diff --git a/font/src/ft/ftwrap.rs b/font/src/ft/ftwrap.rs index 66f3c4a33..4dccd3787 100644 --- a/font/src/ft/ftwrap.rs +++ b/font/src/ft/ftwrap.rs @@ -47,6 +47,12 @@ impl Face { ) } + pub fn has_codepoint(&self, cp: char) -> bool { + unsafe { + FT_Get_Char_Index(self.face, cp as u64) != 0 + } + } + pub fn load_and_render_glyph( &mut self, glyph_index: FT_UInt, @@ -92,7 +98,7 @@ impl Library { P: Into>, { let mut face = ptr::null_mut(); - let path =CString::new(path.into())?; + let path = CString::new(path.into())?; let res = unsafe { FT_New_Face(self.lib, path.as_ptr(), face_index, &mut face as *mut _) diff --git a/font/src/ft/mod.rs b/font/src/ft/mod.rs index 2eab358f5..b0741498d 100644 --- a/font/src/ft/mod.rs +++ b/font/src/ft/mod.rs @@ -1,6 +1,7 @@ use failure::Error; pub mod ftwrap; pub mod hbwrap; +pub mod fcwrap; use self::ftwrap::Library; pub struct FTEngine { diff --git a/src/main.rs b/src/main.rs index b030e611c..40664068b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,19 +11,44 @@ use sdl2::pixels::{Color, PixelFormatEnum}; use sdl2::rect::Rect; use sdl2::render::{BlendMode, Texture, TextureCreator}; +use font::ft::fcwrap; use font::ft::ftwrap; use font::ft::hbwrap; use std::mem; use std::slice; -struct Glyph<'a> { - tex: Texture<'a>, - width: u32, - height: u32, +#[derive(Copy, Clone, Debug)] +struct GlyphInfo { + glyph_pos: u32, + cluster: u32, x_advance: i32, y_advance: i32, x_offset: i32, y_offset: i32, +} + +impl GlyphInfo { + pub fn new( + info: &hbwrap::hb_glyph_info_t, + pos: &hbwrap::hb_glyph_position_t, + ) -> GlyphInfo { + GlyphInfo { + glyph_pos: info.codepoint, + cluster: info.cluster, + x_advance: pos.x_advance / 64, + y_advance: pos.y_advance / 64, + x_offset: pos.x_offset / 64, + y_offset: pos.y_offset / 64, + } + } +} + + +struct Glyph<'a> { + tex: Texture<'a>, + width: u32, + height: u32, + info: GlyphInfo, bearing_x: i32, bearing_y: i32, } @@ -32,7 +57,7 @@ impl<'a> Glyph<'a> { fn new( texture_creator: &'a TextureCreator, glyph: &ftwrap::FT_GlyphSlotRec_, - pos: &hbwrap::hb_glyph_position_t, + info: &GlyphInfo, ) -> Result, Error> { let mode: ftwrap::FT_Pixel_Mode = unsafe { mem::transmute(glyph.bitmap.pixel_mode as u32) }; @@ -60,10 +85,7 @@ impl<'a> Glyph<'a> { tex, width: width as u32, height: height as u32, - x_advance: pos.x_advance / 64, - y_advance: pos.y_advance / 64, - x_offset: pos.x_offset / 64, - y_offset: pos.y_offset / 64, + info: *info, bearing_x: glyph.bitmap_left, bearing_y: glyph.bitmap_top, }) @@ -73,79 +95,185 @@ impl<'a> Glyph<'a> { } } +struct FontInfo { + face: ftwrap::Face, + font: hbwrap::Font, +} + +struct FontHolder { + lib: ftwrap::Library, + size: i64, + pattern: fcwrap::Pattern, + font_list: fcwrap::FontSet, + fonts: Vec, +} + +#[derive(Debug)] +struct ShapedCluster { + /// index into FontHolder.fonts + font_idx: usize, + /// holds shaped results + info: Vec, +} + +impl Drop for FontHolder { + fn drop(&mut self) { + // Ensure that we drop the fonts before we drop the + // library, otherwise we will end up faulting + self.fonts.clear(); + } +} + +impl FontHolder { + fn new(size: i64) -> Result { + let mut lib = ftwrap::Library::new()?; + lib.set_lcd_filter( + ftwrap::FT_LcdFilter::FT_LCD_FILTER_DEFAULT, + )?; + + let mut pattern = fcwrap::Pattern::new()?; + pattern.family("Operator Mono SSm Lig")?; + pattern.family("Emoji One")?; + pattern.monospace()?; + pattern.config_substitute(fcwrap::MatchKind::Pattern)?; + pattern.default_substitute(); + let font_list = pattern.sort(true)?; + + Ok(FontHolder { + lib, + size, + font_list, + pattern, + fonts: Vec::new(), + }) + } + + fn load_next_fallback(&mut self) -> Result<(), Error> { + let idx = self.fonts.len(); + let pat = self.font_list.iter().nth(idx).ok_or(failure::err_msg( + "no more fallbacks", + ))?; + let pat = self.pattern.render_prepare(&pat)?; + let file = pat.get_file()?; + + let mut face = self.lib.new_face(file, 0)?; + face.set_char_size(0, self.size * 64, 96, 96)?; + let font = hbwrap::Font::new(&face); + + self.fonts.push(FontInfo { face, font }); + Ok(()) + } + + fn get_font(&mut self, idx: usize) -> Result<&mut FontInfo, Error> { + if idx >= self.fonts.len() { + self.load_next_fallback()?; + ensure!( + idx < self.fonts.len(), + "should not ask for a font later than the next prepared font" + ); + } + + Ok(&mut self.fonts[idx]) + } + + fn shape(&mut self, s: &str) -> Result, Error> { + let features = vec![ + // kerning + hbwrap::feature_from_string("kern")?, + // ligatures + hbwrap::feature_from_string("liga")?, + // contextual ligatures + hbwrap::feature_from_string("clig")?, + ]; + + let mut buf = hbwrap::Buffer::new()?; + buf.set_script(hbwrap::HB_SCRIPT_LATIN); + buf.set_direction(hbwrap::HB_DIRECTION_LTR); + buf.set_language(hbwrap::language_from_string("en")?); + buf.add_str(s); + + let font_idx = 0; + + self.shape_with_font(font_idx, &mut buf, &features)?; + let infos = buf.glyph_infos(); + let positions = buf.glyph_positions(); + + let mut cluster = Vec::new(); + + for (i, info) in infos.iter().enumerate() { + let pos = &positions[i]; + // TODO: if info.codepoint == 0 here then we should + // rebuild that portion of the string and reshape it + // with the next fallback font + cluster.push(GlyphInfo::new(info, pos)); + } + + println!("shaped: {:?}", cluster); + + Ok(vec![ + ShapedCluster { + font_idx, + info: cluster, + }, + ]) + } + + fn shape_with_font( + &mut self, + idx: usize, + buf: &mut hbwrap::Buffer, + features: &Vec, + ) -> Result<(), Error> { + let info = self.get_font(idx)?; + info.font.shape(buf, Some(features.as_slice())); + Ok(()) + } + + fn load_glyph( + &mut self, + font_idx: usize, + glyph_pos: u32, + ) -> Result<&ftwrap::FT_GlyphSlotRec_, Error> { + let info = &mut self.fonts[font_idx]; + info.face.load_and_render_glyph( + glyph_pos, + (ftwrap::FT_LOAD_COLOR) as i32, + ftwrap::FT_Render_Mode::FT_RENDER_MODE_LCD, + ) + } +} + fn glyphs_for_text<'a, T>( texture_creator: &'a TextureCreator, s: &str, ) -> Result>, Error> { - let mut lib = ftwrap::Library::new()?; - lib.set_lcd_filter( - ftwrap::FT_LcdFilter::FT_LCD_FILTER_DEFAULT, - )?; - let mut face = - lib.new_face("/home/wez/.fonts/OperatorMonoLig-Book.otf", 0)?; - face.set_char_size(0, 36 * 64, 96, 96)?; - let mut font = hbwrap::Font::new(&face); - let lang = hbwrap::language_from_string("en")?; - let mut buf = hbwrap::Buffer::new()?; - buf.set_script(hbwrap::HB_SCRIPT_LATIN); - buf.set_direction(hbwrap::HB_DIRECTION_LTR); - buf.set_language(lang); - buf.add_str(s); - let features = vec![ - // kerning - hbwrap::feature_from_string("kern")?, - // ligatures - hbwrap::feature_from_string("liga")?, - // contextual ligatures - hbwrap::feature_from_string("clig")?, - ]; - font.shape(&mut buf, Some(features.as_slice())); - let infos = buf.glyph_infos(); - let positions = buf.glyph_positions(); + let mut font_holder = FontHolder::new(36)?; + let mut result = Vec::new(); + for cluster in font_holder.shape(s)? { + for info in cluster.info.iter() { + if info.glyph_pos == 0 { + println!("skip: no codepoint for this one"); + continue; + } - for (i, info) in infos.iter().enumerate() { - let pos = &positions[i]; - println!( - "info {} glyph_pos={}, cluster={} x_adv={} y_adv={} x_off={} y_off={}", - i, - info.codepoint, - info.cluster, - pos.x_advance, - pos.y_advance, - pos.x_offset, - pos.y_offset - ); + let glyph = + font_holder.load_glyph(cluster.font_idx, info.glyph_pos)?; - let glyph = face.load_and_render_glyph( - info.codepoint, - (ftwrap::FT_LOAD_COLOR) as i32, - ftwrap::FT_Render_Mode::FT_RENDER_MODE_LCD, - )?; + if glyph.bitmap.width == 0 || glyph.bitmap.rows == 0 { + println!("skip: bitmap for this has 0 dimensions {:?}", glyph); + continue; + } - - let g = Glyph::new(texture_creator, glyph, pos)?; - - /* - println!( - "width={} height={} advx={} advy={} bearing={},{}", - g.width, - g.height, - g.x_advance, - g.y_advance, - g.bearing_x, - g.bearing_y - ); */ - - result.push(g); + let g = Glyph::new(texture_creator, glyph, info)?; + result.push(g); + } } - Ok(result) } fn run() -> Result<(), Error> { - let sdl_context = sdl2::init().map_err(failure::err_msg)?; let video_subsys = sdl_context.video().map_err(failure::err_msg)?; let window = video_subsys @@ -155,7 +283,7 @@ fn run() -> Result<(), Error> { .build()?; let mut canvas = window.into_canvas().build()?; let texture_creator = canvas.texture_creator(); - let glyphs = glyphs_for_text(&texture_creator, "foo->bar();")?; + let mut glyphs = glyphs_for_text(&texture_creator, "foo->bar(); ❤")?; for event in sdl_context .event_pump() @@ -176,21 +304,22 @@ fn run() -> Result<(), Error> { let mut x = 10i32; let mut y = 100i32; - for g in glyphs.iter() { + for g in glyphs.iter_mut() { + g.tex.set_color_mod(0xb3, 0xb3, 0xb3); canvas .copy( &g.tex, Some(Rect::new(0, 0, g.width, g.height)), Some(Rect::new( - x + g.x_offset - g.bearing_x, - y - (g.y_offset + g.bearing_y as i32) as i32, + x + g.info.x_offset - g.bearing_x, + y - (g.info.y_offset + g.bearing_y as i32) as i32, g.width, g.height, )), ) .map_err(failure::err_msg)?; - x += g.x_advance; - y += g.y_advance; + x += g.info.x_advance; + y += g.info.y_advance; } canvas.present();