Skip to content

Commit

Permalink
feat: use harfbuzz for shaping ligatures
Browse files Browse the repository at this point in the history
  • Loading branch information
tizee committed Dec 15, 2021
1 parent 463bd23 commit 4e71f7d
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 37 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }

Expand Down
85 changes: 49 additions & 36 deletions src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -211,35 +202,57 @@ impl FontCollection {
.unwrap()
}

fn shape_text(&self, font: &mut HBFont, text: &str) -> Result<Vec<u32>> {
// 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<PositionedGlyph>, 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();

Expand Down
117 changes: 117 additions & 0 deletions src/hb_wrapper.rs
Original file line number Diff line number Diff line change
@@ -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<hb_feature_t> {
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<HBBuffer> {
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)
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ pub mod directories;
pub mod error;
pub mod font;
pub mod formatter;
pub mod hb_wrapper;
pub mod utils;

0 comments on commit 4e71f7d

Please sign in to comment.