use std::path::Path;
use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
use image::{Rgba, RgbaImage};
use imageproc::drawing::draw_text_mut;
use crate::error::{Error, Result};
#[derive(Clone)]
pub struct TextOptions {
pub font_size: f32,
pub color: Rgba<u8>,
pub background: Rgba<u8>,
pub padding: u32,
}
impl Default for TextOptions {
fn default() -> Self {
TextOptions {
font_size: 72.0,
color: Rgba([0, 0, 0, 255]),
background: Rgba([255, 255, 255, 255]),
padding: 40,
}
}
}
#[derive(Clone)]
pub struct Input {
image: RgbaImage,
opaque: bool,
pub layer: u8,
pub xs: f64,
pub ys: f64,
pub xo: f64,
pub yo: f64,
pub xa: f64,
pub ya: f64,
pub in_scale: i32,
pub in_x0: f64,
pub in_y0: f64,
}
impl Default for Input {
fn default() -> Self {
Input {
image: RgbaImage::new(1, 1),
opaque: false,
layer: 1,
xs: 1.0,
ys: 1.0,
xo: 0.0,
yo: 0.0,
xa: 1.0,
ya: 0.0,
in_scale: 0,
in_x0: 0.0,
in_y0: 0.0,
}
}
}
impl Input {
pub fn new() -> Self {
Input::default()
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::load_with_layer(path, 1)
}
pub fn load_with_layer<P: AsRef<Path>>(path: P, layer: u8) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
return Err(Error::FileNotFound(path.to_path_buf()));
}
let img = image::open(path)?;
let opaque = !img.color().has_alpha();
let rgba = img.to_rgba8();
let mut input = Input { image: rgba, opaque, layer, ..Default::default() };
input.compute_scale_params();
Ok(input)
}
pub fn from_image(image: RgbaImage, layer: u8) -> Self {
let opaque = image.pixels().all(|pixel| pixel[3] == 255);
let mut input = Input { image, opaque, layer, ..Default::default() };
input.compute_scale_params();
input
}
pub fn from_text(
text: &str,
layer: u8,
options: &TextOptions,
font_data: &[u8],
) -> Result<Self> {
let font = FontRef::try_from_slice(font_data)
.map_err(|e| Error::TextRender(format!("Failed to load font: {}", e)))?;
let scale = PxScale::from(options.font_size);
let scaled_font = font.as_scaled(scale);
let mut width = 0.0f32;
for c in text.chars() {
let glyph_id = font.glyph_id(c);
width += scaled_font.h_advance(glyph_id);
}
let height = scaled_font.height();
let img_width = (width.ceil() as u32).max(1) + options.padding * 2;
let img_height = (height.ceil() as u32).max(1) + options.padding * 2;
let mut image = RgbaImage::from_pixel(img_width, img_height, options.background);
let x = options.padding as i32;
let y = options.padding as i32;
draw_text_mut(&mut image, options.color, x, y, scale, &font, text);
let opaque = options.color[3] == 255 && options.background[3] == 255;
let mut input = Input { image, opaque, layer, ..Default::default() };
input.compute_scale_params();
Ok(input)
}
fn compute_scale_params(&mut self) {
let w = self.image.width() as i32;
let h = self.image.height() as i32;
self.in_scale = w;
self.in_x0 = 0.0;
self.in_y0 = 0.0;
if h > w {
self.in_scale = h;
self.in_x0 = -((-w + h) as f64) / 2.0;
} else {
self.in_y0 = -((w - h) as f64) / 2.0;
}
self.xa = 1.0;
self.ya = 0.0;
}
pub fn get(&self) -> &RgbaImage {
&self.image
}
pub fn get_mut(&mut self) -> &mut RgbaImage {
self.opaque = false;
&mut self.image
}
pub fn is_opaque(&self) -> bool {
self.opaque
}
pub fn width(&self) -> u32 {
self.image.width()
}
pub fn height(&self) -> u32 {
self.image.height()
}
pub fn safe_pixel(&self, x: i32, y: i32) -> Rgba<u8> {
let w = self.image.width() as i32;
let h = self.image.height() as i32;
if x < 0 || y < 0 || x >= w || y >= h {
Rgba([0, 0, 0, 0])
} else {
*self.image.get_pixel(x as u32, y as u32)
}
}
pub fn set_rotation(&mut self, theta: f64) {
self.xa = theta.cos();
self.ya = theta.sin();
}
}
#[derive(Clone, Default)]
pub struct Inputs {
data: Vec<Input>,
}
impl Inputs {
pub fn new() -> Self {
Inputs { data: Vec::new() }
}
pub fn add(&mut self) -> &mut Input {
self.data.push(Input::new());
self.data.last_mut().unwrap()
}
pub fn push(&mut self, input: Input) {
self.data.push(input);
}
pub fn get(&self) -> &[Input] {
&self.data
}
pub fn get_mut(&mut self) -> &mut [Input] {
&mut self.data
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Input> {
self.data.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Input> {
self.data.iter_mut()
}
}
impl std::ops::Index<usize> for Inputs {
type Output = Input;
fn index(&self, index: usize) -> &Self::Output {
&self.data[index]
}
}
impl std::ops::IndexMut<usize> for Inputs {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.data[index]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opacity_is_detected_and_invalidated_by_mutable_access() {
let image = RgbaImage::from_pixel(2, 2, Rgba([10, 20, 30, 255]));
let mut input = Input::from_image(image, 1);
assert!(input.is_opaque());
input.get_mut().put_pixel(0, 0, Rgba([10, 20, 30, 255]));
assert!(!input.is_opaque());
let image = RgbaImage::from_pixel(2, 2, Rgba([10, 20, 30, 254]));
assert!(!Input::from_image(image, 1).is_opaque());
}
#[test]
fn text_opacity_follows_foreground_and_background_alpha() {
let font = include_bytes!("../../../fonts/DejaVuSans-Bold.ttf");
let opaque = Input::from_text("opaque", 1, &TextOptions::default(), font)
.expect("render opaque text");
assert!(opaque.is_opaque());
for options in [
TextOptions { color: Rgba([0, 0, 0, 254]), ..Default::default() },
TextOptions { background: Rgba([255, 255, 255, 254]), ..Default::default() },
] {
let transparent =
Input::from_text("transparent", 1, &options, font).expect("render text");
assert!(!transparent.is_opaque());
}
}
}