pub mod builder;
pub mod hsl;
pub mod parts;
pub mod skin;
pub mod uv;
use std::{collections::HashMap, io::Cursor};
use bytes::Bytes;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageReader, RgbaImage, imageops};
use tracing::{debug, error, info, instrument, trace, warn};
use crate::{
error::{Result, TeeError},
tee::{
hsl::{HSL, img_hsl_transform},
parts::{EyeType, EyeTypeData, TeePart, WithShadow},
skin::{Skin, SkinPS},
uv::{TEE_UV_LAYOUT, UV, UVPart},
},
};
#[derive(Debug, Clone, PartialEq)]
pub struct Tee {
pub body: WithShadow,
pub feet: WithShadow,
pub eye: [EyeTypeData; 6],
pub hand: WithShadow,
pub used_uv: UV,
}
impl Tee {
#[instrument(level = "info", skip(data), fields(format = ?format))]
pub fn new(
data: Bytes,
format: ImageFormat,
) -> Result<Self> {
Self::new_with_uv(data, TEE_UV_LAYOUT, format)
}
#[instrument(level = "info", skip(data, uv), fields(format = ?format))]
pub fn new_with_uv(
data: Bytes,
uv: UV,
format: ImageFormat,
) -> Result<Self> {
trace!("Starting to decode image with format: {:?}", format);
let img = decode_image(data, format)?;
let img_dimensions = img.dimensions();
debug!(image_dimensions = ?img_dimensions, "Image decoded successfully.");
validate_image_dimensions(img_dimensions, uv.container)?;
debug!("Extracting all parts from the image.");
let body = extract_with_shadow(&img, uv.body, uv.body_shadow)?;
let feet = extract_with_shadow(&img, uv.feet, uv.feet_shadow)?;
let hand = extract_with_shadow(&img, uv.hand, uv.hand_shadow)?;
let eye = extract_all_eyes(&img, &uv.eyes)?;
info!("Successfully parsed all Tee parts from the image.");
Ok(Self {
body,
feet,
eye,
hand,
used_uv: uv,
})
}
#[cfg(feature = "net")]
#[cfg_attr(docsrs, doc(cfg(feature = "net")))]
#[instrument(level = "info", skip(uv), fields(url = %url))]
pub async fn new_from_url_with_uv(
url: &str,
uv: UV,
) -> Result<Self> {
trace!("Fetching image from URL: {}", url);
let (bytes, format) = fetch_image_from_url(url).await?;
info!(
"Successfully fetched image data, size: {} bytes",
bytes.len()
);
tokio::task::spawn_blocking(move || Self::new_with_uv(bytes, uv, format))
.await
.map_err(TeeError::Join)?
}
#[cfg(feature = "net")]
#[cfg_attr(docsrs, doc(cfg(feature = "net")))]
#[instrument(level = "info", fields(url = %url))]
pub async fn new_from_url(url: &str) -> Result<Self> {
trace!("Fetching image from URL: {}", url);
let (bytes, format) = fetch_image_from_url(url).await?;
info!(
"Successfully fetched image data, size: {} bytes",
bytes.len()
);
tokio::task::spawn_blocking(move || Self::new(bytes, format))
.await
.map_err(TeeError::Join)?
}
#[instrument(level = "debug", skip(self), fields(hsl = ?hsl, parts_count = parts.len()))]
pub fn apply_hsl_to_parts(
&mut self,
hsl: (f32, f32, f32),
parts: &[TeePart],
) {
trace!("Applying HSL transformation to {} parts", parts.len());
for part in parts {
match part {
TeePart::Body => {
img_hsl_transform(&mut self.body.value, hsl);
}
TeePart::BodyShadow => {
img_hsl_transform(&mut self.body.shadow, hsl);
}
TeePart::Feet => {
img_hsl_transform(&mut self.feet.value, hsl);
}
TeePart::FeetShadow => {
img_hsl_transform(&mut self.feet.shadow, hsl);
}
TeePart::Hand => {
img_hsl_transform(&mut self.hand.value, hsl);
}
TeePart::HandShadow => {
img_hsl_transform(&mut self.hand.shadow, hsl);
}
}
}
debug!("Successfully applied HSL transformation to specified parts");
}
#[instrument(level = "debug", skip(self), fields(hls = ?hls))]
pub fn apply_hsl_to_all(
&mut self,
hls: HSL,
) {
trace!("Applying HSL transformation to all parts");
self.apply_hsl_to_parts(
hls,
&[
TeePart::Body,
TeePart::BodyShadow,
TeePart::Feet,
TeePart::FeetShadow,
TeePart::Hand,
TeePart::HandShadow,
],
);
debug!("Successfully applied HSL transformation to all parts");
}
#[instrument(level = "info", skip(self, skin), fields(eye_type = ?eye_type, img_format = ?img_format, skin_container = ?skin.container))]
pub fn compose(
&self,
skin: Skin,
eye_type: EyeType,
img_format: ImageFormat,
) -> Result<Bytes> {
trace!("Starting composition process");
let mut canvas = RgbaImage::new(skin.container.0, skin.container.1);
let mut compose = |layer: &RgbaImage, ((x, y), scale): SkinPS, uv_part: UVPart| {
debug!(
"Composing layer at position ({}, {}) with size ({}, {}) and scale {}",
x, y, uv_part.w, uv_part.h, scale
);
let (w, h) = skin::scale((uv_part.w, uv_part.h), scale);
imageops::overlay(
&mut canvas,
&imageops::resize(layer, w, h, imageops::FilterType::Triangle),
x,
y,
);
};
self.compose_layers(&mut compose, &skin, eye_type);
let mut buf = Vec::new();
let mut cursor = Cursor::new(&mut buf);
debug!(
"Writing composed image to buffer in format: {:?}",
img_format
);
canvas.write_to(&mut cursor, img_format)?;
info!(output_size = buf.len(), "Successfully composed Tee image.");
Ok(Bytes::from(buf))
}
#[instrument(level = "info", skip(self, skin), fields(skin_container = ?skin.container))]
pub fn compose_png(
&self,
skin: Skin,
eye_type: EyeType,
) -> Result<Bytes> {
trace!("Composing with default options (happy eyes, PNG format)");
self.compose(skin, eye_type, ImageFormat::Png)
}
#[instrument(level = "debug", skip(self), fields(eye_type = ?r#type))]
pub fn get_eye(
&self,
r#type: EyeType,
) -> &RgbaImage {
let index = r#type.index();
match (&r#type, &self.eye[index]) {
(EyeType::Normal, EyeTypeData::Normal(img)) => img,
(EyeType::Angry, EyeTypeData::Angry(img)) => img,
(EyeType::Pain, EyeTypeData::Pain(img)) => img,
(EyeType::Happy, EyeTypeData::Happy(img)) => img,
(EyeType::Empty, EyeTypeData::Empty(img)) => img,
(EyeType::Surprise, EyeTypeData::Surprise(img)) => img,
_ => unreachable!(
"Invariant violation: eye type at index {} does not match the requested type.",
index
),
}
}
#[instrument(level = "debug", skip(self))]
pub fn get_all_parts(&self) -> HashMap<TeePart, &WithShadow> {
trace!("Collecting all parts into a HashMap");
let mut parts = HashMap::new();
parts.insert(TeePart::Body, &self.body);
parts.insert(TeePart::Feet, &self.feet);
parts.insert(TeePart::Hand, &self.hand);
debug!("Successfully collected all parts into a HashMap");
parts
}
#[instrument(level = "debug", skip(self))]
pub fn get_all_eyes(&self) -> HashMap<EyeType, &RgbaImage> {
trace!("Collecting all eye types into a HashMap");
let mut eyes = HashMap::new();
eyes.insert(EyeType::Normal, self.get_eye(EyeType::Normal));
eyes.insert(EyeType::Angry, self.get_eye(EyeType::Angry));
eyes.insert(EyeType::Pain, self.get_eye(EyeType::Pain));
eyes.insert(EyeType::Happy, self.get_eye(EyeType::Happy));
eyes.insert(EyeType::Empty, self.get_eye(EyeType::Empty));
eyes.insert(EyeType::Surprise, self.get_eye(EyeType::Surprise));
debug!("Successfully collected all eye types into a HashMap");
eyes
}
fn compose_layers<F>(
&self,
compose: &mut F,
skin: &Skin,
eye_type: EyeType,
) where
F: FnMut(&RgbaImage, SkinPS, UVPart),
{
trace!("Starting to compose layers in order");
compose(&self.body.shadow, skin.body, self.used_uv.body_shadow); compose(&self.feet.shadow, skin.feet_back, self.used_uv.feet_shadow); compose(&self.feet.shadow, skin.feet, self.used_uv.feet_shadow); compose(&self.feet.value, skin.feet_back, self.used_uv.feet); compose(&self.body.value, skin.body, self.used_uv.body);
let eye = self.get_eye(eye_type);
compose(eye, skin.first_eyes, self.used_uv.eyes[0]); compose(
&imageops::flip_horizontal(eye),
skin.second_eyes,
self.used_uv.eyes[0],
);
compose(&self.feet.value, skin.feet, self.used_uv.feet);
debug!("Successfully composed all layers");
}
}
#[instrument(level = "debug", skip(img), fields(part = ?part))]
fn extract_part(
img: &DynamicImage,
part: UVPart,
) -> Result<RgbaImage> {
let (img_width, img_height) = img.dimensions();
if part.x + part.w > img_width || part.y + part.h > img_height {
error!(
image_width = img_width,
image_height = img_height,
"Failed to extract part: out of bounds."
);
return Err(TeeError::OutOfBounds {
part,
width: img_width,
height: img_height,
});
}
trace!(
"Extracting part at position ({}, {}) with size ({}, {})",
part.x, part.y, part.w, part.h
);
let cropped_image = img.view(part.x, part.y, part.w, part.h).to_image();
Ok(cropped_image)
}
#[instrument(level = "debug", skip(data), fields(format = ?format, data_size = data.len()))]
fn decode_image(
data: Bytes,
format: ImageFormat,
) -> Result<DynamicImage> {
let mut img = ImageReader::new(Cursor::new(data));
img.set_format(format);
let img = img.decode()?;
Ok(img)
}
#[instrument(level = "debug", fields(actual = ?actual, expected = ?expected))]
fn validate_image_dimensions(
actual: (u32, u32),
expected: (u32, u32),
) -> Result<()> {
if actual != expected {
error!(
expected = ?expected,
found = ?actual,
"Invalid image dimensions."
);
return Err(TeeError::InvalidDimensions {
expected,
found: actual,
});
}
Ok(())
}
#[instrument(level = "debug", skip(img), fields(part = ?part, shadow_part = ?shadow_part))]
fn extract_with_shadow(
img: &DynamicImage,
part: UVPart,
shadow_part: UVPart,
) -> Result<WithShadow> {
trace!("Extracting part and its shadow");
let value = extract_part(img, part)?;
let shadow = extract_part(img, shadow_part)?;
Ok(WithShadow {
value,
shadow,
})
}
#[instrument(level = "debug", skip(img, eye_parts))]
fn extract_all_eyes(
img: &DynamicImage,
eye_parts: &[UVPart; 6],
) -> Result<[EyeTypeData; 6]> {
trace!("Extracting all eye types");
let eyes = [
EyeTypeData::Normal(extract_part(img, eye_parts[0])?),
EyeTypeData::Angry(extract_part(img, eye_parts[1])?),
EyeTypeData::Pain(extract_part(img, eye_parts[2])?),
EyeTypeData::Happy(extract_part(img, eye_parts[3])?),
EyeTypeData::Empty(extract_part(img, eye_parts[4])?),
EyeTypeData::Surprise(extract_part(img, eye_parts[5])?),
];
Ok(eyes)
}
#[cfg(feature = "net")]
#[instrument(level = "info", fields(url = %url))]
async fn fetch_image_from_url(url: &str) -> Result<(Bytes, ImageFormat)> {
let response = reqwest::get(url).await.map_err(|e| {
error!(error = %e, "Failed to send request.");
TeeError::Reqwest(e)
})?;
let format = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|mime| ImageFormat::from_mime_type(mime))
.ok_or_else(|| {
error!("'Content-Type' header is missing or invalid.");
TeeError::ReqWithOutContentType(url.to_string())
})?;
info!(determined_format = ?format, "Image format determined from response header.");
let bytes = response.bytes().await.map_err(|e| {
error!(error = %e, "Failed to read bytes from response.");
TeeError::Reqwest(e)
})?;
Ok((bytes, format))
}