/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use freetype as ft;
use crate::gfx::{FreetypeFace, Rectangle};
// From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
//
// * An `FT_Face' object can only be safely used from one thread at
// a time.
//
// * An `FT_Library' object can now be used without modification
// from multiple threads at the same time.
//
// * `FT_Face' creation and destruction with the same `FT_Library'
// object can only be done from one thread at a time.
//
// One can use a single `FT_Library' object across threads as long
// as a mutex lock is used around `FT_New_Face' and `FT_Done_Face'.
// Any calls to `FT_Load_Glyph' and similar API are safe and do not
// need the lock to be held as long as the same `FT_Face' is not
// used from multiple threads at the same time.
// Harfbuzz is threadsafe.
// Notes:
// * All ft init and face creation should happen at startup.
// * FT faces protected behind an async Mutex
// * Glyph cache. Key is (glyph_id, font_size)
// * Glyph texture cache: (glyph_id, font_size, color)
#[derive(Clone)]
pub struct Glyph {
pub id: u32,
// Substring this glyph corresponds to
pub substr: String,
// The texture
pub bmp: Vec,
pub bmp_width: u16,
pub bmp_height: u16,
pub pos: Rectangle,
}
pub struct TextShaper {
pub font_faces: Vec,
}
unsafe impl Send for TextShaper {}
unsafe impl Sync for TextShaper {}
impl TextShaper {
fn split_into_substrs(&self, text: String) -> Vec<(usize, String)> {
let mut current_idx = 0;
let mut current_str = String::new();
let mut substrs = vec![];
'next_char: for chr in text.chars() {
let idx = 'get_idx: {
for i in 0..self.font_faces.len() {
let ft_face = &self.font_faces[i];
if ft_face.get_char_index(chr as usize).is_some() {
break 'get_idx i
}
}
warn!("no font fallback for char: '{}'", chr);
// Skip this char
continue 'next_char
};
if current_idx != idx {
if !current_str.is_empty() {
// Push
substrs.push((current_idx, current_str.clone()));
}
current_str.clear();
current_idx = idx;
}
current_str.push(chr);
}
if !current_str.is_empty() {
// Push
substrs.push((current_idx, current_str));
}
substrs
}
pub fn shape(&self, text: String, font_size: f32, text_color: [f32; 4]) -> Vec {
let substrs = self.split_into_substrs(text.clone());
let mut glyphs: Vec = vec![];
let mut current_x = 0.;
let mut current_y = 0.;
for (face_idx, text) in substrs {
//debug!("substr {}", text);
let face = &self.font_faces[face_idx];
if face.has_fixed_sizes() {
// emojis required a fixed size
//face.set_char_size(109 * 64, 0, 72, 72).unwrap();
face.select_size(0).unwrap();
} else {
face.set_char_size(font_size as isize * 64, 0, 72, 72).unwrap();
}
let hb_font = harfbuzz_rs::Font::from_freetype_face(face.clone());
let buffer = harfbuzz_rs::UnicodeBuffer::new()
.set_cluster_level(harfbuzz_rs::ClusterLevel::MonotoneCharacters)
.add_str(&text);
let output = harfbuzz_rs::shape(&hb_font, buffer, &[]);
let positions = output.get_glyph_positions();
let infos = output.get_glyph_infos();
let mut prev_cluster = 0;
for (i, (position, info)) in positions.iter().zip(infos).enumerate() {
let gid = info.codepoint;
// Index within this substr
let curr_cluster = info.cluster as usize;
// Skip first time
if i != 0 {
let substr = text[prev_cluster..curr_cluster].to_string();
glyphs.last_mut().unwrap().substr = substr;
}
prev_cluster = curr_cluster;
let mut flags = ft::face::LoadFlag::DEFAULT;
if face.has_color() {
flags |= ft::face::LoadFlag::COLOR;
}
// FIXME: glyph 884 hangs on android
// For now just avoid using emojis on android
//debug!("load_glyph {}", gid);
face.load_glyph(gid, flags).unwrap();
//debug!("load_glyph {} [done]", gid);
let glyph = face.glyph();
glyph.render_glyph(ft::RenderMode::Normal).unwrap();
let bmp = glyph.bitmap();
let buffer = bmp.buffer();
let bmp_width = bmp.width() as usize;
let bmp_height = bmp.rows() as usize;
let bearing_x = glyph.bitmap_left() as f32;
let bearing_y = glyph.bitmap_top() as f32;
let pixel_mode = bmp.pixel_mode().unwrap();
let bmp = match pixel_mode {
ft::bitmap::PixelMode::Bgra => {
let mut tdata = vec![];
tdata.resize(4 * bmp_width * bmp_height, 0);
// Convert from BGRA to RGBA
for i in 0..bmp_width * bmp_height {
let idx = i * 4;
let b = buffer[idx];
let g = buffer[idx + 1];
let r = buffer[idx + 2];
let a = buffer[idx + 3];
tdata[idx] = r;
tdata[idx + 1] = g;
tdata[idx + 2] = b;
tdata[idx + 3] = a;
}
tdata
}
ft::bitmap::PixelMode::Gray => {
// Convert from greyscale to RGBA8
let tdata: Vec<_> = buffer
.iter()
.flat_map(|coverage| {
let r = (255. * text_color[0]) as u8;
let g = (255. * text_color[1]) as u8;
let b = (255. * text_color[2]) as u8;
let α = ((*coverage as f32) * text_color[3]) as u8;
vec![r, g, b, α]
})
.collect();
tdata
}
_ => panic!("unsupport pixel mode: {:?}", pixel_mode),
};
let pos = if face.has_fixed_sizes() {
// Downscale by height
let w = (bmp_width as f32 * font_size) / bmp_height as f32;
let h = font_size;
// Shouldn't this use the bearing?
let x = current_x;
let y = current_y - h;
current_x += w;
Rectangle { x, y, w, h }
} else {
let (w, h) = (bmp_width as f32, bmp_height as f32);
let off_x = position.x_offset as f32 / 64.;
let off_y = position.y_offset as f32 / 64.;
let x = current_x + off_x + bearing_x;
let y = current_y - off_y - bearing_y;
let x_advance = position.x_advance as f32 / 64.;
let y_advance = position.y_advance as f32 / 64.;
current_x += x_advance;
current_y += y_advance;
Rectangle { x, y, w, h }
};
let glyph = Glyph {
id: gid,
substr: String::new(),
bmp,
bmp_width: bmp_width as u16,
bmp_height: bmp_height as u16,
pos,
};
glyphs.push(glyph);
}
let substr = text[prev_cluster..].to_string();
glyphs.last_mut().unwrap().substr = substr;
}
glyphs
}
}