Added edit_img command
+ Refactored image editing so it's all handled in the same way + Added pipeline concept for more advanced edits
This commit is contained in:
parent
fe99a6f768
commit
dfffe202df
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1093,7 +1093,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fren"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"axum 0.8.1",
|
||||
"base64 0.22.1",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fren"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -1,200 +1,14 @@
|
||||
use crate::album_manager::{AlbumQuery, ImageQuery, ImageSort};
|
||||
use crate::album_manager::AlbumQuery;
|
||||
use crate::discord::Context;
|
||||
use crate::error::Error;
|
||||
use crate::models::motivation::{Motivation, MotivationConfig};
|
||||
use magick_rust::{CompositeOperator, DrawingWand, FilterType, MagickWand, PixelWand};
|
||||
use crate::image_manipulation::{
|
||||
BaseImage, ModifyImage, ModifyImageArgError, create_green_screen_image,
|
||||
create_motivation_image, create_overlay_image, image_pipeline,
|
||||
};
|
||||
use crate::models::motivation::MotivationConfig;
|
||||
use poise::serenity_prelude::builder::{CreateAttachment, CreateMessage};
|
||||
use rand::Rng;
|
||||
use rand::distr::{Distribution, StandardUniform};
|
||||
use reqwest::{Client, Url};
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Alignment {
|
||||
pub fn offset(&self) -> f64 {
|
||||
match self {
|
||||
Alignment::Left => 0.25,
|
||||
Alignment::Center => 0.5,
|
||||
Alignment::Right => 0.75,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Distribution<Alignment> for StandardUniform {
|
||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Alignment {
|
||||
match rng.random_range(0..=2) {
|
||||
0 => Alignment::Left,
|
||||
1 => Alignment::Center,
|
||||
_ => Alignment::Right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_greenscreen_image(
|
||||
background_image_url: Url,
|
||||
foreground_image_url: Url,
|
||||
alignment: Alignment,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let client = Client::new();
|
||||
|
||||
let background_image_blob = client
|
||||
.get(background_image_url)
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?
|
||||
.to_vec();
|
||||
|
||||
let foreground_image_blob = client
|
||||
.get(foreground_image_url)
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?
|
||||
.to_vec();
|
||||
|
||||
let background_wand = MagickWand::new();
|
||||
let foreground_wand = MagickWand::new();
|
||||
|
||||
background_wand.read_image_blob(background_image_blob)?;
|
||||
foreground_wand.read_image_blob(foreground_image_blob)?;
|
||||
|
||||
let background_height = background_wand.get_image_height();
|
||||
let background_width = background_wand.get_image_width();
|
||||
|
||||
let foreground_height = foreground_wand.get_image_height();
|
||||
let foreground_width = foreground_wand.get_image_width();
|
||||
|
||||
let ratio = foreground_height as f64 / foreground_width as f64;
|
||||
|
||||
let (scale_height, scale_width) =
|
||||
if foreground_height > background_height && foreground_width > background_width {
|
||||
(
|
||||
background_height as f64 / foreground_height as f64,
|
||||
background_width as f64 / foreground_width as f64,
|
||||
)
|
||||
} else if foreground_height > background_height {
|
||||
let scale = background_height as f64 / foreground_height as f64;
|
||||
|
||||
(scale, scale * ratio)
|
||||
} else if foreground_width > background_width {
|
||||
let scale = background_width as f64 / foreground_width as f64;
|
||||
|
||||
(scale * ratio, scale)
|
||||
} else {
|
||||
(1.0, 1.0)
|
||||
};
|
||||
|
||||
foreground_wand.scale_image(scale_width * 0.6, scale_height * 0.6, FilterType::Lanczos2)?;
|
||||
|
||||
let pos_x = background_width as f64 * alignment.offset()
|
||||
- foreground_wand.get_image_width() as f64 / 2.0;
|
||||
let pos_y = background_height - foreground_wand.get_image_height();
|
||||
background_wand.compose_images(
|
||||
&foreground_wand,
|
||||
CompositeOperator::Atop,
|
||||
true,
|
||||
pos_x as isize,
|
||||
pos_y as isize,
|
||||
)?;
|
||||
|
||||
Ok(background_wand.write_image_blob("png")?)
|
||||
}
|
||||
|
||||
pub async fn create_overlay_image(
|
||||
background_image_url: Url,
|
||||
overlay_image_url: Url,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let client = Client::new();
|
||||
|
||||
let background_image_blob = client
|
||||
.get(background_image_url)
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?
|
||||
.to_vec();
|
||||
|
||||
let overlay_image_blob = client
|
||||
.get(overlay_image_url)
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?
|
||||
.to_vec();
|
||||
|
||||
let background_wand = MagickWand::new();
|
||||
let foreground_wand = MagickWand::new();
|
||||
|
||||
background_wand.read_image_blob(background_image_blob)?;
|
||||
foreground_wand.read_image_blob(overlay_image_blob)?;
|
||||
|
||||
let overlay_height = background_wand.get_image_height();
|
||||
let overlay_width = background_wand.get_image_width();
|
||||
|
||||
foreground_wand.resize_image(overlay_width, overlay_height, FilterType::Lanczos2)?;
|
||||
|
||||
background_wand.compose_images(&foreground_wand, CompositeOperator::Atop, true, 0, 0)?;
|
||||
|
||||
Ok(background_wand.write_image_blob("png")?)
|
||||
}
|
||||
|
||||
pub async fn create_motivation_image(motivation: Motivation) -> Result<Vec<u8>, Error> {
|
||||
let client = Client::new();
|
||||
|
||||
let motivation_image_blob = client
|
||||
.get(motivation.image.link)
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?
|
||||
.to_vec();
|
||||
|
||||
let text = format!("{} {}", motivation.action, motivation.goal);
|
||||
|
||||
let mut wand = MagickWand::new();
|
||||
let mut border_wand = PixelWand::new();
|
||||
wand.read_image_blob(motivation_image_blob)?;
|
||||
|
||||
border_wand.set_color(&motivation.border_color)?;
|
||||
let width = wand.get_image_width();
|
||||
let border = width / 100;
|
||||
wand.border_image(
|
||||
&border_wand,
|
||||
border,
|
||||
border,
|
||||
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
|
||||
)?;
|
||||
|
||||
border_wand.set_color("black")?;
|
||||
let width = wand.get_image_width();
|
||||
let border = width * 20 / 100;
|
||||
wand.border_image(
|
||||
&border_wand,
|
||||
border,
|
||||
border,
|
||||
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
|
||||
)?;
|
||||
|
||||
let text_pos_x = wand.get_image_width() as f64 / 2.0;
|
||||
let text_pos_y = wand.get_image_height() as f64 - (wand.get_image_height() as f64 * 0.05);
|
||||
|
||||
let mut text_wand = DrawingWand::new();
|
||||
let mut text_color_wand = PixelWand::new();
|
||||
text_color_wand.set_color("white")?;
|
||||
text_wand.set_fill_color(&text_color_wand);
|
||||
text_wand.set_font_size(0.07 * (wand.get_image_width() as f64));
|
||||
text_wand.set_text_alignment(magick_rust::AlignType::Center);
|
||||
wand.annotate_image(&text_wand, text_pos_x, text_pos_y, 0.0, &text)?;
|
||||
Ok(wand.write_image_blob("jpg")?)
|
||||
}
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Become motivated
|
||||
#[poise::command(prefix_command, category = "Image")]
|
||||
@ -202,15 +16,8 @@ pub async fn motivation(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Album to use for the motivation"] album_name: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let motivation = MotivationConfig::generate_motivation(
|
||||
&ctx.data().db,
|
||||
&ctx.data().picox,
|
||||
"white",
|
||||
album_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let image = create_motivation_image(motivation).await?;
|
||||
let image =
|
||||
create_motivation_image(&ctx.data().db, &ctx.data().picox, "white", album_name).await?;
|
||||
|
||||
ctx.channel_id()
|
||||
.send_message(
|
||||
@ -234,38 +41,8 @@ pub async fn green_screen(
|
||||
#[description = "Album to use as the foreground image"] foreground: Option<String>,
|
||||
#[description = "Album to use for the background image"] background: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let foreground = ctx
|
||||
.data()
|
||||
.picox
|
||||
.query_image(ImageQuery {
|
||||
album: foreground,
|
||||
tags: vec![],
|
||||
order: ImageSort::Random,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let background = ctx
|
||||
.data()
|
||||
.picox
|
||||
.query_image(ImageQuery {
|
||||
album: background,
|
||||
tags: vec![],
|
||||
order: ImageSort::Random,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let alignment: Alignment = rand::random();
|
||||
|
||||
let image =
|
||||
create_greenscreen_image(background.link.clone(), foreground.link.clone(), alignment)
|
||||
create_green_screen_image(&ctx.data().db, &ctx.data().picox, foreground, background)
|
||||
.await?;
|
||||
|
||||
ctx.channel_id()
|
||||
@ -283,42 +60,15 @@ pub async fn green_screen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Border
|
||||
/// Overlay an image on another
|
||||
#[poise::command(prefix_command, category = "Image")]
|
||||
pub async fn overlay(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Album to use as the border image"] overlay: Option<String>,
|
||||
#[description = "Album to use for the background image"] background: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let foreground = ctx
|
||||
.data()
|
||||
.picox
|
||||
.query_image(ImageQuery {
|
||||
album: overlay,
|
||||
tags: vec![],
|
||||
order: ImageSort::Random,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let background = ctx
|
||||
.data()
|
||||
.picox
|
||||
.query_image(ImageQuery {
|
||||
album: background,
|
||||
tags: vec![],
|
||||
order: ImageSort::Random,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let image = create_overlay_image(background.link.clone(), foreground.link.clone()).await?;
|
||||
let image =
|
||||
create_overlay_image(&ctx.data().db, &ctx.data().picox, overlay, background).await?;
|
||||
|
||||
ctx.channel_id()
|
||||
.send_message(
|
||||
@ -335,6 +85,46 @@ pub async fn overlay(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mix and match editing tricks to create art for the soul
|
||||
#[poise::command(prefix_command, category = "Image")]
|
||||
pub async fn edit_img(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Base image album to use for your master piece, use 'any' if you want a random image"]
|
||||
base_image_album: String,
|
||||
#[description = "One or many edits to make"] pipeline: Vec<String>,
|
||||
) -> Result<(), Error> {
|
||||
let base_image = if base_image_album.eq_ignore_ascii_case("any") {
|
||||
BaseImage::ByAlbum { album_name: None }
|
||||
} else {
|
||||
BaseImage::ByAlbum {
|
||||
album_name: Some(base_image_album),
|
||||
}
|
||||
};
|
||||
|
||||
let pipeline: Result<Vec<ModifyImage>, ModifyImageArgError> = pipeline
|
||||
.iter()
|
||||
.map(|s| ModifyImage::from_str(s.as_str()))
|
||||
.collect();
|
||||
|
||||
let pipeline = pipeline?;
|
||||
|
||||
let image = image_pipeline(&ctx.data().db, &ctx.data().picox, base_image, pipeline).await?;
|
||||
|
||||
ctx.channel_id()
|
||||
.send_message(
|
||||
&ctx,
|
||||
CreateMessage::new()
|
||||
.content("I fear this is, the most perfect art created by humans. its all over, we have peacked:")
|
||||
.add_file(CreateAttachment::bytes(
|
||||
Cow::from(image),
|
||||
"overlay.jpg".to_string(),
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add an album to the pool of images to be used for motivations
|
||||
#[poise::command(prefix_command, category = "Image")]
|
||||
pub async fn motivation_add_album(
|
||||
|
||||
@ -339,6 +339,7 @@ pub async fn run_bot(global_data: GlobalData) {
|
||||
image::motivation_add_album(),
|
||||
image::green_screen(),
|
||||
image::overlay(),
|
||||
image::edit_img(),
|
||||
movie::add_movie(),
|
||||
movie::list_movies(),
|
||||
movie::rate_movie(),
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
use crate::config::GlobalData;
|
||||
use crate::discord::image::create_motivation_image;
|
||||
use crate::discord::{Context, get_role};
|
||||
use crate::error::Error;
|
||||
use crate::image_manipulation::create_motivation_image;
|
||||
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, nft_value};
|
||||
use crate::models::motivation::MotivationConfig;
|
||||
use crate::models::task::{Task, TaskType};
|
||||
use crate::user::{User, UserError};
|
||||
use poise::serenity_prelude::all::{
|
||||
@ -432,14 +431,8 @@ pub async fn restock_shop(
|
||||
tokio::fs::create_dir(&global_data.cfg.nft_path).await?
|
||||
}
|
||||
|
||||
let nft_motivation = MotivationConfig::generate_motivation(
|
||||
&global_data.db,
|
||||
&global_data.picox,
|
||||
"gold",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let nft = create_motivation_image(nft_motivation).await?;
|
||||
let nft =
|
||||
create_motivation_image(&global_data.db, &global_data.picox, "gold", None).await?;
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(&nft);
|
||||
let nft_hash = hasher.finish();
|
||||
|
||||
11
src/error.rs
11
src/error.rs
@ -1,4 +1,5 @@
|
||||
use crate::discord::voices::VoiceError;
|
||||
use crate::image_manipulation::ModifyImageArgError;
|
||||
use crate::user;
|
||||
use magick_rust::MagickError;
|
||||
use serde::ser::StdError;
|
||||
@ -22,6 +23,8 @@ pub enum Error {
|
||||
IoError(std::io::Error),
|
||||
VoiceError(VoiceError),
|
||||
CTAError(cta_api::Error),
|
||||
NoImageFound,
|
||||
PipelineArgumentError(ModifyImageArgError),
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
@ -86,6 +89,12 @@ impl From<cta_api::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ModifyImageArgError> for Error {
|
||||
fn from(value: ModifyImageArgError) -> Self {
|
||||
Self::PipelineArgumentError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@ -103,6 +112,8 @@ impl Display for Error {
|
||||
Error::IoError(err) => write!(f, "IO Error: {}", err),
|
||||
Error::VoiceError(err) => write!(f, "Voice error: {}", err),
|
||||
Error::CTAError(err) => write!(f, "The CTA had an error, I'm shocked: {}", err),
|
||||
Error::NoImageFound => write!(f, "Image not found"),
|
||||
Error::PipelineArgumentError(err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
387
src/image_manipulation/mod.rs
Normal file
387
src/image_manipulation/mod.rs
Normal file
@ -0,0 +1,387 @@
|
||||
use crate::album_manager::{AlbumManager, ImageQuery, ImageSort};
|
||||
use crate::error::Error;
|
||||
use crate::models::motivation::{MotivationConfig};
|
||||
use j_db::database::Database;
|
||||
use j_db::error::JDbError;
|
||||
use magick_rust::{CompositeOperator, DrawingWand, FilterType, MagickWand, PixelWand};
|
||||
use rand::Rng;
|
||||
use rand::distr::{Distribution, StandardUniform};
|
||||
use reqwest::{Client, Url};
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
pub async fn create_motivation_image(
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
color: &str,
|
||||
album_name: Option<String>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
image_pipeline(
|
||||
db,
|
||||
picox,
|
||||
BaseImage::Motivation { album_name },
|
||||
vec![ModifyImage::Motivation {
|
||||
border_color: Some(color.to_string()),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_green_screen_image(
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
foreground_album_name: Option<String>,
|
||||
background_album_name: Option<String>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
image_pipeline(
|
||||
db,
|
||||
picox,
|
||||
BaseImage::ByAlbum {
|
||||
album_name: foreground_album_name,
|
||||
},
|
||||
vec![ModifyImage::GreenScreen {
|
||||
align: rand::random(),
|
||||
background_album: background_album_name,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_overlay_image(
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
overlay_album_name: Option<String>,
|
||||
background_album_name: Option<String>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
image_pipeline(
|
||||
db,
|
||||
picox,
|
||||
BaseImage::ByAlbum {
|
||||
album_name: background_album_name,
|
||||
},
|
||||
vec![ModifyImage::Overlay {
|
||||
overlay_album: overlay_album_name,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_image_from_url(url: Url) -> Result<Vec<u8>, Error> {
|
||||
let client = Client::new();
|
||||
let image_blob = client.get(url).send().await?.bytes().await?.to_vec();
|
||||
|
||||
Ok(image_blob)
|
||||
}
|
||||
|
||||
pub async fn get_album_image(
|
||||
picox: &AlbumManager,
|
||||
album: Option<String>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let img = picox
|
||||
.query_image(ImageQuery {
|
||||
album,
|
||||
tags: vec![],
|
||||
order: ImageSort::Random,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or(Error::NoImageFound)?;
|
||||
|
||||
get_image_from_url(img.link).await
|
||||
}
|
||||
|
||||
async fn apply_greenscreen(
|
||||
picox: &AlbumManager,
|
||||
img_blob: Vec<u8>,
|
||||
background_album: Option<String>,
|
||||
alignment: &Alignment,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let background_image_blob = get_album_image(picox, background_album).await?;
|
||||
|
||||
let background_wand = MagickWand::new();
|
||||
let foreground_wand = MagickWand::new();
|
||||
|
||||
background_wand.read_image_blob(background_image_blob)?;
|
||||
foreground_wand.read_image_blob(img_blob)?;
|
||||
|
||||
let background_height = background_wand.get_image_height();
|
||||
let background_width = background_wand.get_image_width();
|
||||
|
||||
let foreground_height = foreground_wand.get_image_height();
|
||||
let foreground_width = foreground_wand.get_image_width();
|
||||
|
||||
let ratio = foreground_height as f64 / foreground_width as f64;
|
||||
|
||||
let (scale_height, scale_width) =
|
||||
if foreground_height > background_height && foreground_width > background_width {
|
||||
(
|
||||
background_height as f64 / foreground_height as f64,
|
||||
background_width as f64 / foreground_width as f64,
|
||||
)
|
||||
} else if foreground_height > background_height {
|
||||
let scale = background_height as f64 / foreground_height as f64;
|
||||
|
||||
(scale, scale * ratio)
|
||||
} else if foreground_width > background_width {
|
||||
let scale = background_width as f64 / foreground_width as f64;
|
||||
|
||||
(scale * ratio, scale)
|
||||
} else {
|
||||
(1.0, 1.0)
|
||||
};
|
||||
|
||||
foreground_wand.scale_image(scale_width * 0.6, scale_height * 0.6, FilterType::Lanczos2)?;
|
||||
|
||||
let pos_x = background_width as f64 * alignment.offset()
|
||||
- foreground_wand.get_image_width() as f64 / 2.0;
|
||||
let pos_y = background_height - foreground_wand.get_image_height();
|
||||
background_wand.compose_images(
|
||||
&foreground_wand,
|
||||
CompositeOperator::Atop,
|
||||
true,
|
||||
pos_x as isize,
|
||||
pos_y as isize,
|
||||
)?;
|
||||
|
||||
Ok(background_wand.write_image_blob("png")?)
|
||||
}
|
||||
|
||||
async fn apply_overlay(
|
||||
picox: &AlbumManager,
|
||||
img_blob: Vec<u8>,
|
||||
overlay_album: Option<String>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let overlay_image_blob = get_album_image(picox, overlay_album).await?;
|
||||
|
||||
let background_wand = MagickWand::new();
|
||||
let foreground_wand = MagickWand::new();
|
||||
|
||||
background_wand.read_image_blob(img_blob)?;
|
||||
foreground_wand.read_image_blob(overlay_image_blob)?;
|
||||
|
||||
let overlay_height = background_wand.get_image_height();
|
||||
let overlay_width = background_wand.get_image_width();
|
||||
|
||||
foreground_wand.resize_image(overlay_width, overlay_height, FilterType::Lanczos2)?;
|
||||
|
||||
background_wand.compose_images(&foreground_wand, CompositeOperator::Atop, true, 0, 0)?;
|
||||
|
||||
Ok(background_wand.write_image_blob("png")?)
|
||||
}
|
||||
|
||||
async fn apply_motivation_layer(
|
||||
db: &Database,
|
||||
img_blob: Vec<u8>,
|
||||
border_color: Option<String>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let motivation =
|
||||
MotivationConfig::generate_motivation(db, &border_color.unwrap_or("white".to_string()))
|
||||
.await?;
|
||||
|
||||
let text = format!("{} {}", motivation.action, motivation.goal);
|
||||
|
||||
let mut wand = MagickWand::new();
|
||||
let mut border_wand = PixelWand::new();
|
||||
wand.read_image_blob(img_blob)?;
|
||||
|
||||
border_wand.set_color(&motivation.border_color)?;
|
||||
let width = wand.get_image_width();
|
||||
let border = width / 100;
|
||||
wand.border_image(
|
||||
&border_wand,
|
||||
border,
|
||||
border,
|
||||
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
|
||||
)?;
|
||||
|
||||
border_wand.set_color("black")?;
|
||||
let width = wand.get_image_width();
|
||||
let border = width * 20 / 100;
|
||||
wand.border_image(
|
||||
&border_wand,
|
||||
border,
|
||||
border,
|
||||
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
|
||||
)?;
|
||||
|
||||
let text_pos_x = wand.get_image_width() as f64 / 2.0;
|
||||
let text_pos_y = wand.get_image_height() as f64 - (wand.get_image_height() as f64 * 0.05);
|
||||
|
||||
let mut text_wand = DrawingWand::new();
|
||||
let mut text_color_wand = PixelWand::new();
|
||||
text_color_wand.set_color("white")?;
|
||||
text_wand.set_fill_color(&text_color_wand);
|
||||
text_wand.set_font_size(0.07 * (wand.get_image_width() as f64));
|
||||
text_wand.set_text_alignment(magick_rust::AlignType::Center);
|
||||
wand.annotate_image(&text_wand, text_pos_x, text_pos_y, 0.0, &text)?;
|
||||
Ok(wand.write_image_blob("jpg")?)
|
||||
}
|
||||
|
||||
pub enum ModifyImage {
|
||||
GreenScreen {
|
||||
align: Alignment,
|
||||
background_album: Option<String>,
|
||||
},
|
||||
Overlay {
|
||||
overlay_album: Option<String>,
|
||||
},
|
||||
Motivation {
|
||||
border_color: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ModifyImage {
|
||||
pub async fn process_image_blob(
|
||||
&self,
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
img_blob: Vec<u8>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
match self {
|
||||
ModifyImage::GreenScreen {
|
||||
align,
|
||||
background_album,
|
||||
} => apply_greenscreen(picox, img_blob, background_album.clone(), align).await,
|
||||
ModifyImage::Overlay { overlay_album } => {
|
||||
apply_overlay(picox, img_blob, overlay_album.clone()).await
|
||||
}
|
||||
ModifyImage::Motivation { border_color } => {
|
||||
apply_motivation_layer(db, img_blob, border_color.clone()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BaseImage {
|
||||
Motivation { album_name: Option<String> },
|
||||
ByAlbum { album_name: Option<String> },
|
||||
}
|
||||
|
||||
pub async fn image_pipeline(
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
base_image: BaseImage,
|
||||
img_pipeline: Vec<ModifyImage>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let base_image = match base_image {
|
||||
BaseImage::Motivation { album_name } => {
|
||||
MotivationConfig::get_motivation_image(db, picox, album_name).await?
|
||||
}
|
||||
BaseImage::ByAlbum { album_name } => picox
|
||||
.query_image(ImageQuery {
|
||||
album: album_name,
|
||||
tags: vec![],
|
||||
order: ImageSort::Random,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(Error::from(JDbError::NotFound))?
|
||||
.clone(),
|
||||
};
|
||||
|
||||
let mut img_blob = get_image_from_url(base_image.link).await?;
|
||||
|
||||
for step in &img_pipeline {
|
||||
img_blob = step.process_image_blob(db, picox, img_blob).await?;
|
||||
}
|
||||
|
||||
Ok(img_blob)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ModifyImageArgError {
|
||||
#[error("No type found matching")]
|
||||
NoTypeFound,
|
||||
#[error("Unknown modification type '{0}'")]
|
||||
UnknownType(String),
|
||||
#[error("Not all arguments provided")]
|
||||
NotAllArgumentsSupplied,
|
||||
#[error("{0}")]
|
||||
InvalidAlignment(String),
|
||||
}
|
||||
|
||||
impl FromStr for ModifyImage {
|
||||
type Err = ModifyImageArgError;
|
||||
|
||||
/// type:arg1,arg2
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut s_split = s.split(":");
|
||||
let mod_type = s_split.next().ok_or(Self::Err::NoTypeFound)?.to_lowercase();
|
||||
let mut args = s_split.next().unwrap_or("").split(",");
|
||||
|
||||
match mod_type.as_str() {
|
||||
"greenscreen" => {
|
||||
if args.clone().count() < 1 {
|
||||
Err(Self::Err::NotAllArgumentsSupplied)
|
||||
} else {
|
||||
let alignment_str = args.next().unwrap();
|
||||
let alignment: Alignment = Alignment::from_str(alignment_str)
|
||||
.map_err(Self::Err::InvalidAlignment)?;
|
||||
let background_album = args.next().map(|s| s.to_string());
|
||||
|
||||
Ok(Self::GreenScreen {
|
||||
align: alignment,
|
||||
background_album,
|
||||
})
|
||||
}
|
||||
}
|
||||
"overlay" => {
|
||||
let overlay_album = args.next().map(|s| s.to_string());
|
||||
|
||||
Ok(Self::Overlay { overlay_album })
|
||||
}
|
||||
"motivation" => {
|
||||
let border_color = args.next().map(|s| s.to_string());
|
||||
|
||||
Ok(Self::Motivation { border_color })
|
||||
}
|
||||
_ => Err(Self::Err::UnknownType(mod_type.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Alignment {
|
||||
pub fn offset(&self) -> f64 {
|
||||
match self {
|
||||
Alignment::Left => 0.25,
|
||||
Alignment::Center => 0.5,
|
||||
Alignment::Right => 0.75,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Distribution<Alignment> for StandardUniform {
|
||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Alignment {
|
||||
match rng.random_range(0..=2) {
|
||||
0 => Alignment::Left,
|
||||
1 => Alignment::Center,
|
||||
_ => Alignment::Right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Alignment {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.to_lowercase();
|
||||
match s.as_str() {
|
||||
"left" => Ok(Alignment::Left),
|
||||
"right" => Ok(Alignment::Right),
|
||||
"center" => Ok(Alignment::Center),
|
||||
_ => Err("Unknown alignment".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ mod album_manager;
|
||||
mod config;
|
||||
mod discord;
|
||||
mod error;
|
||||
mod image_manipulation;
|
||||
mod inventory;
|
||||
mod migrations;
|
||||
mod models;
|
||||
|
||||
@ -58,13 +58,28 @@ impl MotivationConfig {
|
||||
|
||||
pub async fn generate_motivation(
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
border_color: &str,
|
||||
album_name: Option<String>,
|
||||
) -> Result<Motivation, Error> {
|
||||
let motivation = Self::get_motivation_config(db)?;
|
||||
let actions = RandomConfig::get_random(db, ACTION_RANDOM_GROUP)?.unwrap_or_default();
|
||||
let goals = RandomConfig::get_random(db, GOAL_RANDOM_GROUP)?.unwrap_or_default();
|
||||
|
||||
let action = actions.get_response(db)?.unwrap_or_default();
|
||||
let goal = goals.get_response(db)?.unwrap_or_default();
|
||||
|
||||
Ok(Motivation {
|
||||
action: action.to_string(),
|
||||
goal: goal.to_string(),
|
||||
border_color: border_color.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_motivation_image(
|
||||
db: &Database,
|
||||
picox: &AlbumManager,
|
||||
album_name: Option<String>,
|
||||
) -> Result<Image, Error> {
|
||||
let mut images = Vec::new();
|
||||
let motivation = Self::get_motivation_config(db)?;
|
||||
|
||||
if let Some(album_name) = album_name {
|
||||
let query = AlbumQuery {
|
||||
@ -92,26 +107,12 @@ impl MotivationConfig {
|
||||
|
||||
let image = images.choose(&mut rng()).ok_or(Error::NoAlbumFound)?;
|
||||
|
||||
let actions = RandomConfig::get_random(db, ACTION_RANDOM_GROUP)?.unwrap_or_default();
|
||||
let goals = RandomConfig::get_random(db, GOAL_RANDOM_GROUP)?.unwrap_or_default();
|
||||
|
||||
let action = actions.get_response(db)?.unwrap_or_default();
|
||||
let goal = goals.get_response(db)?.unwrap_or_default();
|
||||
|
||||
let image = picox.get_image_by_id(*image).await?;
|
||||
|
||||
Ok(Motivation {
|
||||
image,
|
||||
action: action.to_string(),
|
||||
goal: goal.to_string(),
|
||||
border_color: border_color.to_string(),
|
||||
})
|
||||
picox.get_image_by_id(*image).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Motivation {
|
||||
pub image: Image,
|
||||
pub action: String,
|
||||
pub goal: String,
|
||||
pub border_color: String,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user