use std::{collections::BTreeMap, path::Path};
use image::{Pixel, Rgba, RgbaImage, imageops::FilterType};
use imageproc::geometric_transformations::{Interpolation, rotate_about_center};
use serde::{Deserialize, Serialize};
use crate::{
context::AppContext,
error::{DoomError, Result},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlacementConfig {
#[serde(default)]
pub targets: BTreeMap<String, String>,
#[serde(default)]
pub placements: Vec<Placement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Placement {
pub id: String,
pub target: String,
pub logo: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: Option<f32>,
pub rotation_degrees: Option<f32>,
pub opacity: Option<f32>,
pub enabled: Option<bool>,
}
impl Placement {
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
pub fn load_config(ctx: &AppContext, path: impl AsRef<Path>) -> Result<PlacementConfig> {
let path = ctx.repo().repo_path(path);
if !path.exists() {
return Err(DoomError::message(format!(
"Missing placement config: {}",
path.display()
)));
}
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
}
pub fn composite_logo(base: &mut RgbaImage, logo: &RgbaImage, placement: &Placement) {
let draw_width = (placement.width * base.width() as f32).round().max(1.0) as u32;
let draw_height = placement
.height
.map(|value| (value * base.height() as f32).round().max(1.0) as u32)
.unwrap_or_else(|| ((draw_width as f32) * (logo.height() as f32 / logo.width() as f32)).round().max(1.0) as u32);
let mut resized = image::imageops::resize(logo, draw_width, draw_height, FilterType::Lanczos3);
apply_opacity(&mut resized, placement.opacity.unwrap_or(1.0));
let rotation_radians = -placement.rotation_degrees.unwrap_or(0.0).to_radians();
let rotated = if rotation_radians.abs() < f32::EPSILON {
resized
} else {
rotate_about_center(
&resized,
rotation_radians,
Interpolation::Bicubic,
Rgba([0, 0, 0, 0]),
)
};
let center_x = placement.x * base.width() as f32;
let center_y = placement.y * base.height() as f32;
let paste_x = (center_x - rotated.width() as f32 / 2.0).round() as i64;
let paste_y = (center_y - rotated.height() as f32 / 2.0).round() as i64;
alpha_overlay(base, &rotated, paste_x, paste_y);
}
fn apply_opacity(image: &mut RgbaImage, opacity: f32) {
let opacity = opacity.clamp(0.0, 1.0);
if opacity >= 1.0 {
return;
}
for pixel in image.pixels_mut() {
let alpha = ((pixel[3] as f32) * opacity).round().clamp(0.0, 255.0) as u8;
pixel[3] = alpha;
}
}
fn alpha_overlay(base: &mut RgbaImage, overlay: &RgbaImage, offset_x: i64, offset_y: i64) {
for (x, y, pixel) in overlay.enumerate_pixels() {
if pixel[3] == 0 {
continue;
}
let target_x = offset_x + x as i64;
let target_y = offset_y + y as i64;
if target_x < 0
|| target_y < 0
|| target_x >= base.width() as i64
|| target_y >= base.height() as i64
{
continue;
}
let target_pixel = base.get_pixel_mut(target_x as u32, target_y as u32);
target_pixel.blend(pixel);
}
}