use crate::{SubtitleError, SubtitleResult};
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
use fontdue::{Font as FontdueFont, FontSettings};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnicodeScript {
Latin,
Cjk,
Arabic,
Devanagari,
Other,
}
#[must_use]
pub fn script_of(c: char) -> UnicodeScript {
let cp = c as u32;
match cp {
0x0000..=0x024F => UnicodeScript::Latin,
0x0600..=0x06FF | 0x0750..=0x077F | 0xFB50..=0xFDFF | 0xFE70..=0xFEFF => {
UnicodeScript::Arabic
}
0x0900..=0x097F => UnicodeScript::Devanagari,
0x4E00..=0x9FFF
| 0x3400..=0x4DBF
| 0x20000..=0x2A6DF
| 0x2A700..=0x2CEAF
| 0x2CEB0..=0x2EBEF
| 0xF900..=0xFAFF => UnicodeScript::Cjk,
0x3040..=0x30FF => UnicodeScript::Cjk,
0xAC00..=0xD7AF | 0x1100..=0x11FF => UnicodeScript::Cjk,
_ => UnicodeScript::Other,
}
}
pub struct Font {
inner: FontdueFont,
}
impl Font {
pub fn from_bytes(data: Vec<u8>) -> SubtitleResult<Self> {
let inner = FontdueFont::from_bytes(data, FontSettings::default())
.map_err(|e| SubtitleError::FontError(format!("Failed to load font: {e}")))?;
Ok(Self { inner })
}
pub fn from_file(path: &str) -> SubtitleResult<Self> {
let data = std::fs::read(path)
.map_err(|e| SubtitleError::FontError(format!("Failed to read font file: {e}")))?;
Self::from_bytes(data)
}
#[must_use]
pub(crate) fn inner(&self) -> &FontdueFont {
&self.inner
}
#[must_use]
pub fn metrics(&self, size: f32) -> FontMetrics {
let metrics = self
.inner
.horizontal_line_metrics(size)
.unwrap_or(fontdue::LineMetrics {
ascent: 0.0,
descent: 0.0,
line_gap: 0.0,
new_line_size: 0.0,
});
FontMetrics {
ascent: metrics.ascent,
descent: metrics.descent,
line_gap: metrics.line_gap,
new_line_size: metrics.new_line_size,
}
}
#[must_use]
pub fn measure_text(&self, text: &str, size: f32) -> f32 {
let mut width = 0.0;
for c in text.chars() {
let metrics = self.inner.metrics(c, size);
width += metrics.advance_width;
}
width
}
}
#[derive(Clone, Copy, Debug)]
pub struct FontMetrics {
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
pub new_line_size: f32,
}
#[derive(Clone, Debug)]
pub struct CachedGlyph {
pub bitmap: Vec<u8>,
pub width: usize,
pub height: usize,
pub offset_x: f32,
pub offset_y: f32,
pub advance_width: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct GlyphKey {
codepoint: u32,
size_scaled: u32,
}
impl GlyphKey {
fn new(codepoint: char, size: f32) -> Self {
Self {
codepoint: codepoint as u32,
size_scaled: (size * 100.0) as u32,
}
}
}
pub struct GlyphCache {
cache: HashMap<GlyphKey, CachedGlyph>,
font: Font,
}
impl GlyphCache {
#[must_use]
pub fn new(font: Font) -> Self {
Self {
cache: HashMap::new(),
font,
}
}
pub fn get_glyph(&mut self, c: char, size: f32) -> &CachedGlyph {
let key = GlyphKey::new(c, size);
self.cache.entry(key).or_insert_with(|| {
let (metrics, bitmap) = self.font.inner.rasterize(c, size);
CachedGlyph {
bitmap,
width: metrics.width,
height: metrics.height,
offset_x: metrics.xmin as f32,
offset_y: metrics.ymin as f32,
advance_width: metrics.advance_width,
}
})
}
pub fn clear(&mut self) {
self.cache.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.cache.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
#[must_use]
pub fn font(&self) -> &Font {
&self.font
}
}
pub struct ChainedFont {
pub font: Font,
pub preferred_scripts: Vec<UnicodeScript>,
}
impl ChainedFont {
#[must_use]
pub fn universal(font: Font) -> Self {
Self {
font,
preferred_scripts: Vec::new(),
}
}
#[must_use]
pub fn for_scripts(font: Font, scripts: Vec<UnicodeScript>) -> Self {
Self {
font,
preferred_scripts: scripts,
}
}
#[must_use]
pub fn covers(&self, script: UnicodeScript) -> bool {
self.preferred_scripts.is_empty() || self.preferred_scripts.contains(&script)
}
}
pub struct FontChain {
primary: ChainedFont,
fallbacks: Vec<ChainedFont>,
cache: HashMap<(u32, u32), CachedGlyph>,
}
impl FontChain {
#[must_use]
pub fn new(primary: Font) -> Self {
Self {
primary: ChainedFont::universal(primary),
fallbacks: Vec::new(),
cache: HashMap::new(),
}
}
pub fn add(&mut self, font: ChainedFont) {
self.fallbacks.push(font);
}
pub fn rasterize(&mut self, c: char, size: f32) -> &CachedGlyph {
let key = (c as u32, (size * 100.0) as u32);
if !self.cache.contains_key(&key) {
let script = script_of(c);
let glyph = self.rasterize_inner(c, size, script);
self.cache.insert(key, glyph);
}
match self.cache.get(&key) {
Some(glyph) => glyph,
None => unreachable!("cache key was just confirmed or inserted"),
}
}
fn rasterize_inner(&self, c: char, size: f32, script: UnicodeScript) -> CachedGlyph {
if self.primary.covers(script) {
let (metrics, bitmap) = self.primary.font.inner.rasterize(c, size);
if !bitmap.is_empty() || metrics.width > 0 {
return make_cached_glyph(metrics, bitmap);
}
}
for fb in &self.fallbacks {
if fb.covers(script) && !fb.preferred_scripts.is_empty() {
let (metrics, bitmap) = fb.font.inner.rasterize(c, size);
if !bitmap.is_empty() || metrics.width > 0 {
return make_cached_glyph(metrics, bitmap);
}
}
}
for fb in &self.fallbacks {
if fb.preferred_scripts.is_empty() {
let (metrics, bitmap) = fb.font.inner.rasterize(c, size);
if !bitmap.is_empty() || metrics.width > 0 {
return make_cached_glyph(metrics, bitmap);
}
}
}
let (metrics, bitmap) = self.primary.font.inner.rasterize(c, size);
make_cached_glyph(metrics, bitmap)
}
#[must_use]
pub fn measure_text(&self, text: &str, size: f32) -> f32 {
let mut total = 0.0_f32;
for c in text.chars() {
let script = script_of(c);
let width = self.advance_width_for(c, size, script);
total += width;
}
total
}
fn advance_width_for(&self, c: char, size: f32, script: UnicodeScript) -> f32 {
if self.primary.covers(script) {
let m = self.primary.font.inner.metrics(c, size);
if m.advance_width > 0.0 {
return m.advance_width;
}
}
for fb in &self.fallbacks {
if fb.covers(script) {
let m = fb.font.inner.metrics(c, size);
if m.advance_width > 0.0 {
return m.advance_width;
}
}
}
self.primary.font.inner.metrics(c, size).advance_width
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
#[must_use]
pub fn cache_len(&self) -> usize {
self.cache.len()
}
}
fn make_cached_glyph(metrics: fontdue::Metrics, bitmap: Vec<u8>) -> CachedGlyph {
CachedGlyph {
bitmap,
width: metrics.width,
height: metrics.height,
offset_x: metrics.xmin as f32,
offset_y: metrics.ymin as f32,
advance_width: metrics.advance_width,
}
}
#[cfg(test)]
mod font_chain_tests {
use super::*;
#[test]
fn test_script_of_latin() {
assert_eq!(script_of('A'), UnicodeScript::Latin);
assert_eq!(script_of('z'), UnicodeScript::Latin);
assert_eq!(script_of('0'), UnicodeScript::Latin);
}
#[test]
fn test_script_of_cjk_main_block() {
assert_eq!(script_of('\u{4E2D}'), UnicodeScript::Cjk);
}
#[test]
fn test_script_of_cjk_hiragana() {
assert_eq!(script_of('\u{3042}'), UnicodeScript::Cjk);
}
#[test]
fn test_script_of_cjk_hangul() {
assert_eq!(script_of('\u{AC00}'), UnicodeScript::Cjk);
}
#[test]
fn test_script_of_arabic() {
assert_eq!(script_of('\u{0627}'), UnicodeScript::Arabic);
}
#[test]
fn test_script_of_devanagari() {
assert_eq!(script_of('\u{0905}'), UnicodeScript::Devanagari);
}
#[test]
fn test_script_of_other() {
assert_eq!(script_of('\u{2603}'), UnicodeScript::Other);
}
#[test]
fn test_chained_font_universal_covers_all() {
let covers_struct = ChainedFontCoversTester {
preferred_scripts: vec![],
};
assert!(covers_struct.covers(UnicodeScript::Cjk));
assert!(covers_struct.covers(UnicodeScript::Arabic));
assert!(covers_struct.covers(UnicodeScript::Latin));
assert!(covers_struct.covers(UnicodeScript::Other));
}
#[test]
fn test_chained_font_specific_covers() {
let covers_struct = ChainedFontCoversTester {
preferred_scripts: vec![UnicodeScript::Cjk],
};
assert!(covers_struct.covers(UnicodeScript::Cjk));
assert!(!covers_struct.covers(UnicodeScript::Arabic));
assert!(!covers_struct.covers(UnicodeScript::Latin));
}
#[test]
fn test_script_of_cjk_extension() {
assert_eq!(script_of('\u{3400}'), UnicodeScript::Cjk);
}
#[test]
fn test_script_of_katakana() {
assert_eq!(script_of('\u{30A2}'), UnicodeScript::Cjk);
}
#[test]
fn test_script_of_arabic_presentation() {
assert_eq!(script_of('\u{FB50}'), UnicodeScript::Arabic);
}
#[test]
fn test_script_of_arabic_extended() {
assert_eq!(script_of('\u{FE70}'), UnicodeScript::Arabic);
}
#[test]
fn test_script_of_latin_extended() {
assert_eq!(script_of('\u{0100}'), UnicodeScript::Latin);
}
struct ChainedFontCoversTester {
preferred_scripts: Vec<UnicodeScript>,
}
impl ChainedFontCoversTester {
fn covers(&self, script: UnicodeScript) -> bool {
self.preferred_scripts.is_empty() || self.preferred_scripts.contains(&script)
}
}
}
pub struct SimpleLayoutEngine {
layout: Layout,
}
impl SimpleLayoutEngine {
#[must_use]
pub fn new() -> Self {
Self {
layout: Layout::new(CoordinateSystem::PositiveYDown),
}
}
pub fn layout_text(
&mut self,
font: &Font,
text: &str,
size: f32,
max_width: Option<f32>,
) -> Vec<GlyphPosition> {
self.layout.reset(&LayoutSettings {
x: 0.0,
y: 0.0,
max_width,
max_height: None,
horizontal_align: fontdue::layout::HorizontalAlign::Left,
vertical_align: fontdue::layout::VerticalAlign::Top,
line_height: size * 1.2,
wrap_style: fontdue::layout::WrapStyle::Word,
wrap_hard_breaks: true,
});
let fonts = &[font.inner()];
self.layout.append(fonts, &TextStyle::new(text, size, 0));
self.layout
.glyphs()
.iter()
.map(|g| GlyphPosition {
c: g.parent,
x: g.x,
y: g.y,
width: g.width as f32,
height: g.height as f32,
})
.collect()
}
#[must_use]
pub fn bounds(&self) -> (f32, f32) {
let glyphs = self.layout.glyphs();
if glyphs.is_empty() {
return (0.0, 0.0);
}
let max_x = glyphs
.iter()
.map(|g| g.x + g.width as f32)
.fold(0.0_f32, f32::max);
let max_y = glyphs
.iter()
.map(|g| g.y + g.height as f32)
.fold(0.0_f32, f32::max);
(max_x, max_y)
}
}
impl Default for SimpleLayoutEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug)]
pub struct GlyphPosition {
pub c: char,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}