use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
const PATCH_PX: u32 = 28;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageImage {
pub id: Uuid,
pub name: String,
pub source: String,
pub added_at: DateTime<Utc>,
pub mime: String,
pub width: u32,
pub height: u32,
pub bytes: usize,
pub data: String,
}
impl MessageImage {
pub fn new(
name: impl Into<String>,
source: impl Into<String>,
mime: impl Into<String>,
width: u32,
height: u32,
data: String,
) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
source: source.into(),
added_at: Utc::now(),
mime: mime.into(),
width,
height,
bytes: base64_decoded_len(&data),
data,
}
}
pub fn est_tokens(&self) -> usize {
let w = self.width.div_ceil(PATCH_PX) as usize;
let h = self.height.div_ceil(PATCH_PX) as usize;
w * h
}
pub fn matches(&self, target: &str) -> bool {
let same = super::chat_file::same_name;
same(&self.name, target) || same(&self.source, target)
}
}
fn base64_decoded_len(b64: &str) -> usize {
let len = b64.len();
if len == 0 {
return 0;
}
let padding = b64.bytes().rev().take_while(|&c| c == b'=').count();
len / 4 * 3 - padding
}
pub fn resolve_target(items: &[MessageImage], target: &str) -> super::attachment::Resolved {
super::attachment::resolve_handle(items, target, MessageImage::matches)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageInfo {
pub name: String,
pub source: String,
pub mime: String,
pub width: u32,
pub height: u32,
pub bytes: usize,
pub est_tokens: usize,
}
impl From<&MessageImage> for ImageInfo {
fn from(image: &MessageImage) -> Self {
Self {
name: image.name.clone(),
source: image.source.clone(),
mime: image.mime.clone(),
width: image.width,
height: image.height,
bytes: image.bytes,
est_tokens: image.est_tokens(),
}
}
}
pub fn infos(images: &[MessageImage]) -> Vec<ImageInfo> {
images.iter().map(ImageInfo::from).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn image(name: &str, w: u32, h: u32) -> MessageImage {
MessageImage::new(
name,
format!("D:\\pics\\{name}"),
"image/png",
w,
h,
"AAAA".to_string(),
)
}
#[test]
fn est_tokens_counts_patches_and_rounds_up() {
assert_eq!(image("a.png", 28, 28).est_tokens(), 1);
assert_eq!(image("a.png", 29, 28).est_tokens(), 2);
assert_eq!(image("a.png", 1568, 1568).est_tokens(), 56 * 56);
}
#[test]
fn decoded_len_accounts_for_padding() {
assert_eq!(base64_decoded_len("AAAA"), 3);
assert_eq!(base64_decoded_len("AAA="), 2);
assert_eq!(base64_decoded_len("AA=="), 1);
assert_eq!(base64_decoded_len(""), 0);
assert_eq!(image("a.png", 8, 8).bytes, 3);
}
#[test]
fn matches_by_name_or_path_case_insensitively() {
let i = image("Chart.PNG", 8, 8);
assert!(i.matches("chart.png"));
assert!(i.matches("d:\\pics\\Chart.PNG"));
assert!(!i.matches("other.png"));
assert!(image("Отчёт.png", 8, 8).matches("отчёт.png"));
}
#[test]
fn resolve_target_by_index_name_and_path() {
use crate::entities::attachment::Resolved;
let items = vec![image("a.png", 8, 8), image("b.png", 8, 8)];
assert_eq!(resolve_target(&items, "#1"), Resolved::One(0));
assert_eq!(resolve_target(&items, "#2"), Resolved::One(1));
assert_eq!(resolve_target(&items, "b.png"), Resolved::One(1));
assert_eq!(resolve_target(&items, "\"b.png\""), Resolved::One(1));
assert_eq!(resolve_target(&items, "D:\\pics\\a.png"), Resolved::One(0));
assert_eq!(resolve_target(&items, "#0"), Resolved::Nothing);
assert_eq!(resolve_target(&items, "#3"), Resolved::Nothing);
assert_eq!(resolve_target(&items, "nope.png"), Resolved::Nothing);
assert_eq!(resolve_target(&items, "2"), Resolved::One(1));
assert_eq!(resolve_target(&items, "3"), Resolved::Nothing);
}
#[test]
fn info_carries_the_card_but_not_the_payload() {
let i = image("a.png", 56, 28);
let info = ImageInfo::from(&i);
assert_eq!(info.name, "a.png");
assert_eq!((info.width, info.height), (56, 28));
assert_eq!(info.est_tokens, 2);
}
#[test]
fn serde_roundtrip() {
let i = image("a.png", 56, 28);
let json = serde_json::to_string(&i).unwrap();
let back: MessageImage = serde_json::from_str(&json).unwrap();
assert_eq!(i, back);
}
}