use crate::{SubtitleError, SubtitleResult};
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
use fontdue::{Font as FontdueFont, FontSettings};
use std::collections::HashMap;
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 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,
}