diff --git a/Cargo.lock b/Cargo.lock index 8c22a74..3dcbe19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -537,6 +537,20 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "harfbuzz-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8c27ca13930dc4ffe474880040fe9e0f03c2121600dc9c95423624cab3e467" +dependencies = [ + "cc", + "core-graphics", + "core-text", + "foreign-types", + "freetype", + "pkg-config", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -1301,6 +1315,7 @@ dependencies = [ "dirs", "env_logger", "font-kit", + "harfbuzz-sys", "image", "imageproc", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 3d46359..c480548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ edition = "2018" [dependencies] dirs = "3.0" imageproc = "0.22.0" -font-kit = "0.10" clipboard = "0.5.0" tempfile = "3.1.0" conv = "0.3.3" @@ -55,6 +54,13 @@ default-features = false features = ["termcolor", "atty", "humantime"] optional = true +[dependencies.font-kit] +version= "0.10" +features= ["loader-freetype-default", "source-fontconfig-default"] + +[dependencies.harfbuzz-sys] +version="0.5.0" + [patch.crates-io] pathfinder_simd = { version = "0.5.0", git = "https://github.com/servo/pathfinder" } diff --git a/src/font.rs b/src/font.rs index 359fa2c..c0a11e3 100644 --- a/src/font.rs +++ b/src/font.rs @@ -12,6 +12,8 @@ //! font.draw_text_mut(&mut image, Rgb([255, 0, 0]), 0, 0, FontStyle::REGULAR, "Hello, world"); //! ``` use crate::error::FontError; +use crate::hb_wrapper::{feature_from_tag, HBBuffer, HBFont}; +use anyhow::Result; use conv::ValueInto; use font_kit::canvas::{Canvas, Format, RasterizationOptions}; use font_kit::font::Font; @@ -191,17 +193,6 @@ impl FontCollection { Ok(Self(fonts)) } - fn glyph_for_char(&self, c: char, style: FontStyle) -> Option<(u32, &ImageFont, &Font)> { - for font in &self.0 { - let result = font.get_by_style(style); - if let Some(id) = result.glyph_for_char(c) { - return Some((id, font, result)); - } - } - eprintln!("[warning] No font found for character `{}`", c); - None - } - /// get max height of all the fonts pub fn get_font_height(&self) -> u32 { self.0 @@ -211,35 +202,57 @@ impl FontCollection { .unwrap() } + fn shape_text(&self, font: &mut HBFont, text: &str) -> Result> { + // feature tags + let features = vec![ + feature_from_tag("kern")?, + feature_from_tag("clig")?, + feature_from_tag("liga")?, + ]; + let mut buf = HBBuffer::new()?; + buf.add_str(text); + buf.guess_segments_properties(); + font.shape(&buf, features.as_slice()); + let hb_infos = buf.get_glyph_infos(); + let mut glyph_ids = Vec::new(); + for info in hb_infos.iter() { + glyph_ids.push(info.codepoint); + } + Ok(glyph_ids) + } + fn layout(&self, text: &str, style: FontStyle) -> (Vec, u32) { let mut delta_x = 0; let height = self.get_font_height(); - let glyphs = text - .chars() - .filter_map(|c| { - self.glyph_for_char(c, style).map(|(id, imfont, font)| { - let raster_rect = font - .raster_bounds( - id, - imfont.size, - Transform2F::default(), - HintingOptions::None, - RasterizationOptions::GrayscaleAa, - ) - .unwrap(); - let position = - Vector2I::new(delta_x as i32, height as i32) + raster_rect.origin(); - delta_x += Self::get_glyph_width(font, id, imfont.size); - - PositionedGlyph { - id, - font: font.clone(), - size: imfont.size, - raster_rect, - position, - } - }) + let imfont = self.0.get(0).unwrap(); + let font = imfont.get_by_style(style); + let mut hb_font = HBFont::new(font); + // apply font features especially ligature with a shape engine + let shaped_glyphs = self.shape_text(&mut hb_font, text).unwrap(); + + let glyphs = shaped_glyphs + .iter() + .map(|id| { + let raster_rect = font + .raster_bounds( + *id, + imfont.size, + Transform2F::default(), + HintingOptions::None, + RasterizationOptions::GrayscaleAa, + ) + .unwrap(); + let position = Vector2I::new(delta_x as i32, height as i32) + raster_rect.origin(); + delta_x += Self::get_glyph_width(font, *id, imfont.size); + + PositionedGlyph { + id: *id, + font: font.clone(), + size: imfont.size, + raster_rect, + position, + } }) .collect(); diff --git a/src/hb_wrapper.rs b/src/hb_wrapper.rs new file mode 100644 index 0000000..771254f --- /dev/null +++ b/src/hb_wrapper.rs @@ -0,0 +1,117 @@ +use anyhow::{ensure, Result}; +use core::slice; +// font_kit already has a wrapper around freetype called Font so use it directly +use font_kit::font::Font; +use font_kit::loaders::freetype::NativeFont; +// use harfbuzz for shaping ligatures +pub use harfbuzz::*; +use harfbuzz_sys as harfbuzz; +use std::mem; + +/// font feature tag +pub fn feature_from_tag(tag: &str) -> Result { + unsafe { + let mut feature = mem::zeroed(); + ensure!( + hb_feature_from_string( + tag.as_ptr() as *const i8, + tag.len() as i32, + &mut feature as *mut _ + ) != 0, + "hb_feature_from_string failed for {}", + tag + ); + Ok(feature) + } +} + +/// Harfbuzz font +pub struct HBFont { + font: *mut hb_font_t, +} + +// harfbuzz freetype integration +extern "C" { + pub fn hb_ft_font_create_referenced(face: NativeFont) -> *mut hb_font_t; // the same as hb_face_t +} + +impl Drop for HBFont { + fn drop(&mut self) { + unsafe { hb_font_destroy(self.font) } + } +} + +impl HBFont { + pub fn new(face: &Font) -> HBFont { + HBFont { + font: unsafe { hb_ft_font_create_referenced(face.native_font() as _) }, + } + } + pub fn shape(&mut self, buffer: &HBBuffer, features: &[hb_feature_t]) { + unsafe { + hb_shape( + self.font, + buffer.buffer, + features.as_ptr(), + features.len() as u32, + ); + } + } +} + +/// Harfbuzz buffer +pub struct HBBuffer { + buffer: *mut hb_buffer_t, +} + +impl Drop for HBBuffer { + fn drop(&mut self) { + unsafe { hb_buffer_destroy(self.buffer) } + } +} + +impl HBBuffer { + pub fn new() -> Result { + let hb_buf = unsafe { hb_buffer_create() }; + ensure!( + unsafe { hb_buffer_allocation_successful(hb_buf) } != 0, + "hb_buffer_create failed!" + ); + Ok(HBBuffer { buffer: hb_buf }) + } + + pub fn guess_segments_properties(&mut self) { + unsafe { hb_buffer_guess_segment_properties(self.buffer) }; + } + + pub fn add_utf8(&mut self, s: &[u8]) { + unsafe { + hb_buffer_add_utf8( + self.buffer, + s.as_ptr() as *const i8, + s.len() as i32, + 0, + s.len() as i32, + ); + } + } + pub fn add_str(&mut self, s: &str) { + self.add_utf8(s.as_bytes()); + } + + pub fn get_glyph_infos(&mut self) -> &[hb_glyph_info_t] { + unsafe { + let mut len: u32 = 0; + let info = hb_buffer_get_glyph_infos(self.buffer, &mut len as *mut u32); + slice::from_raw_parts(info, len as usize) + } + } + + pub fn get_glyph_positions(&mut self) -> &[hb_glyph_position_t] { + unsafe { + let mut len: u32 = 0; + let info = hb_buffer_get_glyph_positions(self.buffer, &mut len as *mut u32); + slice::from_raw_parts(info, len as usize) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7289d85..a9076dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,4 +39,5 @@ pub mod directories; pub mod error; pub mod font; pub mod formatter; +pub mod hb_wrapper; pub mod utils;