#![allow(clippy::cast_precision_loss, clippy::too_many_arguments)]
use crate::{Draw, Error::FontError, Image, OverlayMode, Pixel};
use fontdue::{
layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle},
FontSettings,
};
use std::{fs::File, io::Read, ops::DerefMut, path::Path};
#[allow(clippy::doc_markdown)]
#[derive(Clone)]
pub struct Font {
inner: fontdue::Font,
settings: FontSettings,
}
impl Font {
pub fn open<P: AsRef<Path>>(path: P, optimal_size: f32) -> crate::Result<Self> {
Self::from_reader(File::open(path)?, optimal_size)
}
pub fn from_bytes(bytes: &[u8], optimal_size: f32) -> crate::Result<Self> {
let settings = FontSettings {
scale: optimal_size,
collection_index: 0,
};
let inner = fontdue::Font::from_bytes(bytes, settings).map_err(FontError)?;
Ok(Self { inner, settings })
}
pub fn from_reader<R: Read>(mut buffer: R, optimal_size: f32) -> crate::Result<Self> {
let settings = FontSettings {
scale: optimal_size,
collection_index: 0,
};
let mut out = Vec::new();
buffer.read_to_end(&mut out)?;
let inner = fontdue::Font::from_bytes(out, settings).map_err(FontError)?;
Ok(Self { inner, settings })
}
#[must_use]
pub const fn inner(&self) -> &fontdue::Font {
&self.inner
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn into_inner(self) -> fontdue::Font {
self.inner
}
#[must_use]
pub const fn optimal_size(&self) -> f32 {
self.settings.scale
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WrapStyle {
None,
Word,
Character,
}
impl Default for WrapStyle {
fn default() -> Self {
Self::Word
}
}
#[derive(Clone)]
pub struct TextSegment<'a, P: Pixel> {
pub position: (u32, u32),
pub width: Option<u32>,
pub text: String,
pub font: &'a Font,
pub fill: P,
pub overlay: OverlayMode,
pub size: f32,
pub wrap: WrapStyle,
}
impl<'a, P: Pixel> TextSegment<'a, P> {
#[must_use]
pub fn new(font: &'a Font, text: impl AsRef<str>, fill: P) -> Self {
Self {
position: (0, 0),
width: None,
text: text.as_ref().to_string(),
font,
fill,
overlay: OverlayMode::Merge,
size: font.optimal_size(),
wrap: WrapStyle::Word,
}
}
#[must_use]
pub const fn with_position(mut self, x: u32, y: u32) -> Self {
self.position = (x, y);
self
}
#[must_use]
pub const fn with_size(mut self, size: f32) -> Self {
self.size = size;
self
}
#[must_use]
pub const fn with_overlay_mode(mut self, mode: OverlayMode) -> Self {
self.overlay = mode;
self
}
#[must_use]
pub const fn with_width(mut self, width: u32) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub const fn with_wrap(mut self, wrap: WrapStyle) -> Self {
self.wrap = wrap;
self
}
fn layout(&self) -> Layout<(P, OverlayMode)> {
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.reset(&LayoutSettings {
x: self.position.0 as f32,
y: self.position.1 as f32,
max_width: if self.wrap == WrapStyle::None {
None
} else {
self.width.map(|w| w as f32)
},
wrap_style: match self.wrap {
WrapStyle::None | WrapStyle::Word => fontdue::layout::WrapStyle::Word,
WrapStyle::Character => fontdue::layout::WrapStyle::Letter,
},
..LayoutSettings::default()
});
layout.append(
&[self.font.inner()],
&TextStyle::with_user_data(&self.text, self.size, 0, (self.fill, self.overlay)),
);
layout
}
}
fn render_layout<P: Pixel>(
image: &mut Image<P>,
fonts: &[&fontdue::Font],
layout: &Layout<(P, OverlayMode)>,
) {
let glyphs = layout.glyphs();
if glyphs.is_empty() {
return;
}
let lines = unsafe { layout.lines().unwrap_unchecked() };
for line in lines {
for glyph in &glyphs[line.glyph_start..=line.glyph_end] {
let (fill, overlay) = glyph.user_data;
let font = fonts[glyph.font_index];
let (metrics, bitmap) = font.rasterize_config(glyph.key);
if metrics.width == 0 || glyph.char_data.is_whitespace() || metrics.height == 0 {
continue;
}
for (row, y) in bitmap.chunks_exact(metrics.width).zip(glyph.y as i32..) {
for (value, x) in row.iter().zip(glyph.x as i32..) {
let (x, y) = if x < 0 || y < 0 {
continue;
} else {
(x as u32, y as u32)
};
let value = *value;
if value == 0 {
continue;
}
if let Some(pixel) = image.get_pixel(x, y) {
*image.pixel_mut(x, y) = pixel.overlay_with_alpha(fill, overlay, value);
}
}
}
}
}
}
fn render_layout_with_alignment<P: Pixel>(
image: &mut Image<P>,
fonts: &[&fontdue::Font],
layout: &Layout<(P, OverlayMode)>,
widths: Vec<u32>,
max_width: u32,
fx: f32,
ox: f32,
oy: f32,
) {
let glyphs = layout.glyphs();
if glyphs.is_empty() {
return;
}
let lines = unsafe { layout.lines().unwrap_unchecked() };
for (line, width) in lines.iter().zip(widths) {
let ox = ((max_width - width) as f32).mul_add(fx, ox);
for glyph in &glyphs[line.glyph_start..=line.glyph_end] {
let (fill, overlay) = glyph.user_data;
let font = fonts[glyph.font_index];
let (metrics, bitmap) = font.rasterize_config(glyph.key);
if metrics.width == 0 || glyph.char_data.is_whitespace() || metrics.height == 0 {
continue;
}
let x = (glyph.x + ox) as i32;
let y = (glyph.y + oy) as i32;
for (row, y) in bitmap.chunks_exact(metrics.width).zip(y..) {
for (value, x) in row.iter().zip(x..) {
let (x, y) = if x < 0 || y < 0 {
continue;
} else {
(x as u32, y as u32)
};
let value = *value;
if value == 0 {
continue;
}
if let Some(pixel) = image.get_pixel(x, y) {
*image.pixel_mut(x, y) = pixel.overlay_with_alpha(fill, overlay, value);
}
}
}
}
}
}
impl<'a, P: Pixel> Draw<P> for TextSegment<'a, P> {
fn draw<I: DerefMut<Target = Image<P>>>(&self, mut image: I) {
render_layout(&mut *image, &[self.font.inner()], &self.layout());
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum HorizontalAnchor {
Left,
Center,
Right,
}
impl Default for HorizontalAnchor {
fn default() -> Self {
Self::Left
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
impl Default for TextAlign {
fn default() -> Self {
Self::Left
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum VerticalAnchor {
Top,
Center,
Bottom,
}
impl Default for VerticalAnchor {
fn default() -> Self {
Self::Top
}
}
pub struct TextLayout<'a, P: Pixel> {
inner: Layout<(P, OverlayMode)>,
fonts: Vec<&'a fontdue::Font>,
settings: LayoutSettings,
x_anchor: HorizontalAnchor,
y_anchor: VerticalAnchor,
align: TextAlign,
}
impl<'a, P: Pixel> TextLayout<'a, P> {
#[must_use]
pub fn new() -> Self {
Self {
inner: Layout::new(CoordinateSystem::PositiveYDown),
fonts: Vec::new(),
settings: LayoutSettings::default(),
x_anchor: HorizontalAnchor::default(),
y_anchor: VerticalAnchor::default(),
align: TextAlign::default(),
}
}
fn set_settings(&mut self, settings: LayoutSettings) {
self.inner.reset(&settings);
self.settings = settings;
}
#[must_use]
pub fn with_position(mut self, x: u32, y: u32) -> Self {
self.set_settings(LayoutSettings {
x: x as f32,
y: y as f32,
..self.settings
});
self
}
#[must_use]
pub fn with_wrap(mut self, wrap: WrapStyle) -> Self {
self.set_settings(LayoutSettings {
wrap_style: match wrap {
WrapStyle::None | WrapStyle::Word => fontdue::layout::WrapStyle::Word,
WrapStyle::Character => fontdue::layout::WrapStyle::Letter,
},
max_width: Some(self.settings.max_width.unwrap_or(f32::MAX)),
..self.settings
});
self
}
#[must_use]
pub fn with_width(mut self, width: u32) -> Self {
self.set_settings(LayoutSettings {
max_width: Some(width as f32),
..self.settings
});
self
}
pub fn push_segment(&mut self, segment: &TextSegment<'a, P>) {
self.fonts.push(segment.font.inner());
self.inner.append(
&self.fonts,
&TextStyle::with_user_data(
&segment.text,
segment.size,
self.fonts.len() - 1,
(segment.fill, segment.overlay),
),
);
}
#[must_use]
pub fn with_segment(mut self, segment: &TextSegment<'a, P>) -> Self {
self.push_segment(segment);
self
}
pub fn push_basic_text(&mut self, font: &'a Font, text: impl AsRef<str>, fill: P) {
self.push_segment(&TextSegment::new(font, text, fill));
}
#[must_use]
pub fn with_basic_text(mut self, font: &'a Font, text: impl AsRef<str>, fill: P) -> Self {
self.push_basic_text(font, text, fill);
self
}
#[must_use]
pub const fn with_horizontal_anchor(mut self, anchor: HorizontalAnchor) -> Self {
self.x_anchor = anchor;
self
}
#[must_use]
pub const fn with_vertical_anchor(mut self, anchor: VerticalAnchor) -> Self {
self.y_anchor = anchor;
self
}
#[must_use]
pub const fn with_align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
#[must_use]
pub const fn centered(self) -> Self {
self.with_horizontal_anchor(HorizontalAnchor::Center)
.with_vertical_anchor(VerticalAnchor::Center)
}
fn line_widths(&self) -> (Vec<u32>, u32, u32) {
let glyphs = self.inner.glyphs();
if glyphs.is_empty() {
return (Vec::new(), 0, 0);
}
let mut widths = Vec::new();
let mut max_width = 0;
for line in unsafe { self.inner.lines().unwrap_unchecked() } {
let x = self.settings.x as u32;
let glyph = &glyphs[line.glyph_end];
let right = glyph.x + glyph.width as f32;
let line_width = (right - x as f32).ceil() as u32;
widths.push(line_width);
max_width = max_width.max(line_width);
}
(widths, max_width, self.inner.height() as u32)
}
#[must_use]
pub fn width(&self) -> u32 {
let glyphs = self.inner.glyphs();
if glyphs.is_empty() {
return 0;
}
let mut width = 0;
for line in unsafe { self.inner.lines().unwrap_unchecked() } {
let x = self.settings.x as u32;
for glyph in glyphs[line.glyph_start..=line.glyph_end].iter().rev() {
if glyph.char_data.is_whitespace() {
continue;
}
let right = glyph.x + glyph.width as f32;
let line_width = (right - x as f32).ceil() as u32;
width = width.max(line_width);
break;
}
}
width
}
#[must_use]
pub fn height(&self) -> u32 {
self.inner.height() as u32
}
#[must_use]
pub fn dimensions(&self) -> (u32, u32) {
(self.width(), self.height())
}
#[must_use]
pub fn bounding_box(&self) -> (u32, u32, u32, u32) {
let (width, height) = self.dimensions();
let ox = match self.x_anchor {
HorizontalAnchor::Left => 0.0,
HorizontalAnchor::Center => width as f32 / -2.0,
HorizontalAnchor::Right => -(width as f32),
};
let oy = match self.y_anchor {
VerticalAnchor::Top => 0.0,
VerticalAnchor::Center => height as f32 / -2.0,
VerticalAnchor::Bottom => -(height as f32),
};
let x = (self.settings.x + ox) as u32;
let y = (self.settings.y + oy) as u32;
(x, y, x + width, y + height)
}
fn calculate_offsets(&self) -> (Vec<u32>, u32, f32, f32, f32) {
let (widths, width, height) = self.line_widths();
let fx = match self.align {
TextAlign::Left => 0.0,
TextAlign::Center => 0.5,
TextAlign::Right => 1.0,
};
let ox = match self.x_anchor {
HorizontalAnchor::Left => 0.0,
HorizontalAnchor::Center => width as f32 / -2.0,
HorizontalAnchor::Right => -(width as f32),
};
let oy = match self.y_anchor {
VerticalAnchor::Top => 0.0,
VerticalAnchor::Center => height as f32 / -2.0,
VerticalAnchor::Bottom => -(height as f32),
};
(widths, width, fx, ox, oy)
}
}
impl<'a, P: Pixel> Draw<P> for TextLayout<'a, P> {
fn draw<I: DerefMut<Target = Image<P>>>(&self, mut image: I) {
let image = &mut *image;
if self.x_anchor == HorizontalAnchor::Left
&& self.y_anchor == VerticalAnchor::Top
&& self.align == TextAlign::Left
{
render_layout(image, &self.fonts, &self.inner);
return;
}
let (widths, max_width, fx, ox, oy) = self.calculate_offsets();
render_layout_with_alignment(
image,
&self.fonts,
&self.inner,
widths,
max_width,
fx,
ox,
oy,
);
}
}
impl<'a, P: Pixel> Default for TextLayout<'a, P> {
fn default() -> Self {
Self::new()
}
}