/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2025 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 harfbuzz_sys::{
freetype::hb_ft_font_create_referenced, hb_buffer_add_utf8, hb_buffer_create,
hb_buffer_destroy, hb_buffer_get_glyph_infos, hb_buffer_get_glyph_positions,
hb_buffer_guess_segment_properties, hb_buffer_set_cluster_level, hb_buffer_set_content_type,
hb_font_destroy, hb_glyph_info_t, hb_glyph_position_t, hb_shape,
HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES, HB_BUFFER_CONTENT_TYPE_UNICODE,
};
use std::{
collections::HashMap,
ffi::OsStr,
os,
path::PathBuf,
sync::{Arc, Mutex as SyncMutex, Weak},
};
use crate::gfx::Rectangle;
mod atlas;
pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
mod ft;
use ft::{render_glyph, FreetypeFace, Sprite, SpritePtr};
mod shape;
use shape::{set_face_size, shape};
mod wrap;
pub use wrap::{glyph_str, wrap};
// Upscale emoji relative to font size
pub const EMOJI_SCALE_FACT: f32 = 1.6;
// How much of the emoji is above baseline?
pub const EMOJI_PROP_ABOVE_BASELINE: f32 = 0.8;
#[cfg(target_os = "android")]
fn custom_font_path() -> PathBuf {
crate::android::get_external_storage_path().join("font")
}
#[cfg(not(target_os = "android"))]
fn custom_font_path() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/font")
}
// 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 a Mutex
// * Glyph cache. Key is (glyph_id, font_size)
// * Glyph texture cache: (glyph_id, font_size, color)
#[derive(Clone)]
pub struct GlyphPositionIter<'a> {
font_size: f32,
window_scale: f32,
glyphs: &'a Vec,
current_x: f32,
current_y: f32,
i: usize,
}
impl<'a> GlyphPositionIter<'a> {
pub fn new(font_size: f32, window_scale: f32, glyphs: &'a Vec, baseline_y: f32) -> Self {
let start_y = baseline_y * window_scale;
Self { font_size, window_scale, glyphs, current_x: 0., current_y: start_y, i: 0 }
}
}
impl<'a> Iterator for GlyphPositionIter<'a> {
type Item = Rectangle;
fn next(&mut self) -> Option {
assert!(self.i <= self.glyphs.len());
if self.i == self.glyphs.len() {
return None
}
let glyph = &self.glyphs[self.i];
let sprite = &glyph.sprite;
// current_x/y is scaled real coords
// but the returned rect is unscaled
let rect = if sprite.has_fixed_sizes {
// Downscale by height
let w = (sprite.bmp_width as f32 * EMOJI_SCALE_FACT * self.font_size) /
sprite.bmp_height as f32;
let h = EMOJI_SCALE_FACT * self.font_size;
let x = self.current_x / self.window_scale;
let y = self.current_y / self.window_scale - (EMOJI_PROP_ABOVE_BASELINE * h);
self.current_x += w * self.window_scale;
Rectangle { x, y, w, h }
} else {
let (w, h) = (sprite.bmp_width as f32, sprite.bmp_height as f32);
let off_x = glyph.x_offset as f32 / 64.;
let off_y = glyph.y_offset as f32 / 64.;
let x = self.current_x + off_x + sprite.bearing_x;
let y = self.current_y - off_y - sprite.bearing_y;
let x_advance = glyph.x_advance;
let y_advance = glyph.y_advance;
self.current_x += x_advance;
self.current_y += y_advance;
// Downscale back again
Rectangle { x, y, w, h } / self.window_scale
};
self.i += 1;
Some(rect)
}
}
struct TextShaperInternal {
font_faces: FtFaces,
cache: TextShaperCache,
}
impl TextShaperInternal {
#[inline]
fn faces<'a>(&'a mut self) -> &'a mut Vec {
&mut self.font_faces.0
}
#[inline]
fn face<'a>(&'a mut self, idx: usize) -> &'a mut FreetypeFace {
&mut self.font_faces.0[idx]
}
}
pub struct TextShaper {
intern: SyncMutex,
fonts_data: Vec>,
}
impl TextShaper {
pub fn new() -> Arc {
let ftlib = freetype::Library::init().unwrap();
let mut fonts_data = vec![];
if let Ok(read_dir) = std::fs::read_dir(custom_font_path()) {
for entry in read_dir {
let Ok(entry) = entry else {
warn!(target: "text", "Skipping unknown in custom font path");
continue
};
let font_path = entry.path();
if font_path.is_dir() {
warn!(target: "text", "Skipping {font_path:?} in custom font path: is directory");
continue
}
let Some(font_ext) = font_path.extension().and_then(OsStr::to_str) else {
warn!(target: "text", "Skipping {font_path:?} in custom font path: missing file extension");
continue
};
if !["ttf", "otf"].contains(&font_ext) {
warn!(target: "text", "Skipping {font_path:?} in custom font path: unsupported file extension (supported: ttf, otf)");
continue
}
let font_data: Vec = match std::fs::read(&font_path) {
Ok(font_data) => font_data,
Err(err) => {
warn!(target: "text", "Unexpected error loading {font_path:?} in custom font path: {err}");
continue
}
};
info!(target: "text", "Loaded custom font: {font_path:?}");
fonts_data.push(font_data);
}
}
fonts_data.reserve_exact(fonts_data.len() + 2);
let mut faces = vec![];
let font_data = include_bytes!("../../ibm-plex-mono-regular.otf") as &[u8];
let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
faces.push(ft_face);
for font_data in &fonts_data {
let face = unsafe { Self::load_font_face(&ftlib, font_data) };
faces.push(face);
}
let font_data = include_bytes!("../../NotoColorEmoji.ttf") as &[u8];
let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
faces.push(ft_face);
Arc::new(Self {
intern: SyncMutex::new(TextShaperInternal {
font_faces: FtFaces(faces),
cache: HashMap::new(),
}),
fonts_data,
})
}
/// Beware: recasts font_data as static. Make sure data outlives the face.
unsafe fn load_font_face(ftlib: &freetype::Library, font_data: &[u8]) -> FreetypeFace {
let font_data = &*(font_data as *const _);
let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
ft_face
}
pub fn shape(&self, mut text: String, font_size: f32, window_scale: f32) -> Vec {
//debug!(target: "text", "shape('{}', {})", text, font_size);
if text.is_empty() {
return vec![]
}
let text = &text;
// Freetype faces are not threadsafe
let mut intern = self.intern.lock().unwrap();
let size = font_size * window_scale;
for face in intern.faces() {
set_face_size(face, size);
}
let mut glyphs: Vec = vec![];
'next_glyph: for glyph_info in shape(intern.faces(), text) {
let face_idx = glyph_info.face_idx;
let face = intern.face(face_idx);
let glyph_id = glyph_info.id;
let substr = glyph_info.substr(text).to_string();
let x_offset = glyph_info.x_offset as f32 / 64.;
let y_offset = glyph_info.y_offset as f32 / 64.;
let x_advance = glyph_info.x_advance as f32 / 64.;
let y_advance = glyph_info.y_advance as f32 / 64.;
// Check cache
// If it exists in the cache then skip
// Relevant info:
// * glyph_id
// * font_size (for non-fixed size faces)
// * face_idx
let cache_key = CacheKey {
glyph_id,
font_size: if face.has_fixed_sizes() {
FontSize::Fixed
} else {
FontSize::from((font_size, window_scale))
},
face_idx,
};
//debug!(target: "text", "cache_key: {:?}", cache_key);
'load_sprite: {
if let Some(sprite) = intern.cache.get(&cache_key) {
let Some(sprite) = sprite.upgrade() else {
break 'load_sprite;
};
//debug!(target: "text", "found glyph!");
let glyph = Glyph {
glyph_id,
substr,
sprite,
x_offset,
y_offset,
x_advance,
y_advance,
};
glyphs.push(glyph);
continue 'next_glyph;
}
}
let face = intern.face(face_idx);
let Some(sprite) = render_glyph(&face, glyph_id) else { continue };
let sprite = Arc::new(sprite);
intern.cache.insert(cache_key, Arc::downgrade(&sprite));
let glyph =
Glyph { glyph_id, substr, sprite, x_offset, y_offset, x_advance, y_advance };
//debug!(target: "text", "pushing glyph...");
glyphs.push(glyph);
}
glyphs
}
}
#[derive(Eq, Hash, PartialEq, Debug)]
enum FontSize {
Fixed,
Size((u32, u32)),
}
impl FontSize {
/// You can't use f32 in Hash and Eq impls
fn from(size: (f32, f32)) -> Self {
let font_size = (size.0 * 1000.).round() as u32;
let scale = (size.1 * 1000.).round() as u32;
Self::Size((font_size, scale))
}
}
#[derive(Eq, Hash, PartialEq, Debug)]
struct CacheKey {
glyph_id: u32,
font_size: FontSize,
face_idx: usize,
}
#[derive(Clone)]
pub struct Glyph {
pub glyph_id: u32,
// Substring this glyph corresponds to
pub substr: String,
pub sprite: SpritePtr,
// Normally these are i32, we provide the conversions
pub x_offset: f32,
pub y_offset: f32,
pub x_advance: f32,
pub y_advance: f32,
}
impl std::fmt::Debug for Glyph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Glyph")
.field("glyph_id", &self.glyph_id)
.field("substr", &self.substr)
.finish()
}
}
struct FtFaces(Vec);
unsafe impl Send for FtFaces {}
unsafe impl Sync for FtFaces {}
pub type TextShaperPtr = Arc;
type TextShaperCache = HashMap>;