Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support font ligatures #181

Merged
merged 1 commit into from
Dec 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;