use std::cell::RefCell;
use std::collections::BinaryHeap;
use std::path::Path;
use anyhow::Context;
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle, VerticalAlign};
use levenshtein::levenshtein;
use once_cell::sync::Lazy;
use raqote::{DrawOptions, Point, SolidSource};
use rust_fontconfig::{FcFontCache, FcFontPath, FcPattern};
use super::{DrawTarget, FontBackend, FontColor, Result};
static FONTCONFIG_CACHE: Lazy<FcFontCache> = Lazy::new(FcFontCache::build);
const BUF_SIZE: usize = 256 * 256;
pub struct Font {
inner: fontdue::Font,
layout: RefCell<Layout>,
buffer: RefCell<Vec<u32>>,
}
impl Font {
fn with_font(inner: fontdue::Font) -> Self {
Self {
inner,
layout: RefCell::new(Layout::new(CoordinateSystem::PositiveYDown)),
buffer: RefCell::new(vec![0; BUF_SIZE]),
}
}
}
#[derive(Eq)]
struct FuzzyResult<'a> {
text: &'a str,
distance: usize,
}
impl<'a> PartialEq for FuzzyResult<'a> {
fn eq(&self, other: &Self) -> bool {
self.distance == other.distance
}
}
impl<'a> Ord for FuzzyResult<'a> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.distance.cmp(&other.distance)
}
}
impl<'a> PartialOrd for FuzzyResult<'a> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.distance.partial_cmp(&other.distance)
}
}
impl Font {
fn from_fc_path(font_search: &FcFontPath) -> Result<Self> {
let bytes = std::fs::read(font_search.path.as_str()).context("font read")?;
let inner = fontdue::Font::from_bytes(
bytes,
fontdue::FontSettings {
collection_index: font_search.font_index as u32,
..Default::default()
},
)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(Font::with_font(inner))
}
fn try_find_best_font(name: &str) -> Vec<String> {
const COUNT_MATCHES: usize = 5;
FONTCONFIG_CACHE
.list()
.keys()
.filter_map(|font| {
let text = font.name.as_ref()?.as_str();
Some(FuzzyResult {
text,
distance: levenshtein(name, text),
})
})
.collect::<BinaryHeap<FuzzyResult>>()
.into_sorted_vec()
.into_iter()
.take(COUNT_MATCHES)
.map(|v| v.text.to_owned())
.collect()
}
}
impl FontBackend for Font {
fn default() -> Self {
FONTCONFIG_CACHE
.query(&FcPattern::default())
.map(Font::from_fc_path)
.unwrap()
.unwrap()
}
fn font_by_name(name: &str) -> Result<Self> {
FONTCONFIG_CACHE
.query(&FcPattern {
name: Some(name.to_string()),
..Default::default()
})
.map(Font::from_fc_path)
.ok_or_else(|| {
let matching = Font::try_find_best_font(name);
log::info!("The font {} could not be found.", name);
if !matching.is_empty() {
use itertools::Itertools;
log::info!("Best matches:\n\t{}\n", matching.into_iter().format("\n\t"));
}
anyhow::anyhow!("cannot find font")
})?
}
fn font_by_path(path: &Path) -> Result<Self> {
Font::from_fc_path(&FcFontPath {
path: path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid path"))?
.to_owned(),
font_index: 0,
})
}
fn draw(
&self,
dt: &mut DrawTarget,
text: &str,
font_size: f32,
start_pos: Point,
color: FontColor,
opts: &DrawOptions,
) {
let mut buf = self.buffer.borrow_mut();
let mut layout = self.layout.borrow_mut();
layout.reset(&LayoutSettings {
x: start_pos.x,
y: start_pos.y,
max_height: Some(font_size),
vertical_align: VerticalAlign::Middle,
..LayoutSettings::default()
});
layout.append(&[&self.inner], &TextStyle::new(text, font_size, 0));
for (n, g) in layout.glyphs().iter().enumerate() {
let (_, b) = self.inner.rasterize_config(g.key);
assert!(g.width * g.height <= BUF_SIZE);
let width = g.width as i32;
let height = g.height as i32;
let color = match color {
FontColor::Single(color) => color,
FontColor::Multiple(ref colors) => colors[n],
};
for (i, x) in b.into_iter().enumerate() {
let src = SolidSource::from_unpremultiplied_argb(
(u32::from(x) * u32::from(color.a) / 255) as u8,
color.r,
color.g,
color.b,
);
buf[i] = (u32::from(src.a) << 24)
| (u32::from(src.r) << 16)
| (u32::from(src.g) << 8)
| u32::from(src.b);
}
let img = raqote::Image {
width,
height,
data: &buf[..],
};
dt.draw_image_with_size_at(g.width as f32, g.height as f32, g.x, g.y, &img, opts);
}
}
}