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]]
|
[[package]]
|
||||||
name = "fren"
|
name = "fren"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.8.1",
|
"axum 0.8.1",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fren"
|
name = "fren"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# 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::discord::Context;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::models::motivation::{Motivation, MotivationConfig};
|
use crate::image_manipulation::{
|
||||||
use magick_rust::{CompositeOperator, DrawingWand, FilterType, MagickWand, PixelWand};
|
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 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;
|
use std::borrow::Cow;
|
||||||
|
use std::str::FromStr;
|
||||||
#[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")?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Become motivated
|
/// Become motivated
|
||||||
#[poise::command(prefix_command, category = "Image")]
|
#[poise::command(prefix_command, category = "Image")]
|
||||||
@ -202,15 +16,8 @@ pub async fn motivation(
|
|||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Album to use for the motivation"] album_name: Option<String>,
|
#[description = "Album to use for the motivation"] album_name: Option<String>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let motivation = MotivationConfig::generate_motivation(
|
let image =
|
||||||
&ctx.data().db,
|
create_motivation_image(&ctx.data().db, &ctx.data().picox, "white", album_name).await?;
|
||||||
&ctx.data().picox,
|
|
||||||
"white",
|
|
||||||
album_name,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let image = create_motivation_image(motivation).await?;
|
|
||||||
|
|
||||||
ctx.channel_id()
|
ctx.channel_id()
|
||||||
.send_message(
|
.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 as the foreground image"] foreground: Option<String>,
|
||||||
#[description = "Album to use for the background image"] background: Option<String>,
|
#[description = "Album to use for the background image"] background: Option<String>,
|
||||||
) -> Result<(), Error> {
|
) -> 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 =
|
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?;
|
.await?;
|
||||||
|
|
||||||
ctx.channel_id()
|
ctx.channel_id()
|
||||||
@ -283,42 +60,15 @@ pub async fn green_screen(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Border
|
/// Overlay an image on another
|
||||||
#[poise::command(prefix_command, category = "Image")]
|
#[poise::command(prefix_command, category = "Image")]
|
||||||
pub async fn overlay(
|
pub async fn overlay(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Album to use as the border image"] overlay: Option<String>,
|
#[description = "Album to use as the border image"] overlay: Option<String>,
|
||||||
#[description = "Album to use for the background image"] background: Option<String>,
|
#[description = "Album to use for the background image"] background: Option<String>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let foreground = ctx
|
let image =
|
||||||
.data()
|
create_overlay_image(&ctx.data().db, &ctx.data().picox, overlay, background).await?;
|
||||||
.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?;
|
|
||||||
|
|
||||||
ctx.channel_id()
|
ctx.channel_id()
|
||||||
.send_message(
|
.send_message(
|
||||||
@ -335,6 +85,46 @@ pub async fn overlay(
|
|||||||
Ok(())
|
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
|
/// Add an album to the pool of images to be used for motivations
|
||||||
#[poise::command(prefix_command, category = "Image")]
|
#[poise::command(prefix_command, category = "Image")]
|
||||||
pub async fn motivation_add_album(
|
pub async fn motivation_add_album(
|
||||||
|
|||||||
@ -339,6 +339,7 @@ pub async fn run_bot(global_data: GlobalData) {
|
|||||||
image::motivation_add_album(),
|
image::motivation_add_album(),
|
||||||
image::green_screen(),
|
image::green_screen(),
|
||||||
image::overlay(),
|
image::overlay(),
|
||||||
|
image::edit_img(),
|
||||||
movie::add_movie(),
|
movie::add_movie(),
|
||||||
movie::list_movies(),
|
movie::list_movies(),
|
||||||
movie::rate_movie(),
|
movie::rate_movie(),
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
use crate::config::GlobalData;
|
use crate::config::GlobalData;
|
||||||
use crate::discord::image::create_motivation_image;
|
|
||||||
use crate::discord::{Context, get_role};
|
use crate::discord::{Context, get_role};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::image_manipulation::create_motivation_image;
|
||||||
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, nft_value};
|
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, nft_value};
|
||||||
use crate::models::motivation::MotivationConfig;
|
|
||||||
use crate::models::task::{Task, TaskType};
|
use crate::models::task::{Task, TaskType};
|
||||||
use crate::user::{User, UserError};
|
use crate::user::{User, UserError};
|
||||||
use poise::serenity_prelude::all::{
|
use poise::serenity_prelude::all::{
|
||||||
@ -432,14 +431,8 @@ pub async fn restock_shop(
|
|||||||
tokio::fs::create_dir(&global_data.cfg.nft_path).await?
|
tokio::fs::create_dir(&global_data.cfg.nft_path).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
let nft_motivation = MotivationConfig::generate_motivation(
|
let nft =
|
||||||
&global_data.db,
|
create_motivation_image(&global_data.db, &global_data.picox, "gold", None).await?;
|
||||||
&global_data.picox,
|
|
||||||
"gold",
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let nft = create_motivation_image(nft_motivation).await?;
|
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
hasher.write(&nft);
|
hasher.write(&nft);
|
||||||
let nft_hash = hasher.finish();
|
let nft_hash = hasher.finish();
|
||||||
|
|||||||
11
src/error.rs
11
src/error.rs
@ -1,4 +1,5 @@
|
|||||||
use crate::discord::voices::VoiceError;
|
use crate::discord::voices::VoiceError;
|
||||||
|
use crate::image_manipulation::ModifyImageArgError;
|
||||||
use crate::user;
|
use crate::user;
|
||||||
use magick_rust::MagickError;
|
use magick_rust::MagickError;
|
||||||
use serde::ser::StdError;
|
use serde::ser::StdError;
|
||||||
@ -22,6 +23,8 @@ pub enum Error {
|
|||||||
IoError(std::io::Error),
|
IoError(std::io::Error),
|
||||||
VoiceError(VoiceError),
|
VoiceError(VoiceError),
|
||||||
CTAError(cta_api::Error),
|
CTAError(cta_api::Error),
|
||||||
|
NoImageFound,
|
||||||
|
PipelineArgumentError(ModifyImageArgError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StdError for Error {}
|
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 {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@ -103,6 +112,8 @@ impl Display for Error {
|
|||||||
Error::IoError(err) => write!(f, "IO Error: {}", err),
|
Error::IoError(err) => write!(f, "IO Error: {}", err),
|
||||||
Error::VoiceError(err) => write!(f, "Voice 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::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 config;
|
||||||
mod discord;
|
mod discord;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod image_manipulation;
|
||||||
mod inventory;
|
mod inventory;
|
||||||
mod migrations;
|
mod migrations;
|
||||||
mod models;
|
mod models;
|
||||||
|
|||||||
@ -58,13 +58,28 @@ impl MotivationConfig {
|
|||||||
|
|
||||||
pub async fn generate_motivation(
|
pub async fn generate_motivation(
|
||||||
db: &Database,
|
db: &Database,
|
||||||
picox: &AlbumManager,
|
|
||||||
border_color: &str,
|
border_color: &str,
|
||||||
album_name: Option<String>,
|
|
||||||
) -> Result<Motivation, Error> {
|
) -> 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 mut images = Vec::new();
|
||||||
|
let motivation = Self::get_motivation_config(db)?;
|
||||||
|
|
||||||
if let Some(album_name) = album_name {
|
if let Some(album_name) = album_name {
|
||||||
let query = AlbumQuery {
|
let query = AlbumQuery {
|
||||||
@ -92,26 +107,12 @@ impl MotivationConfig {
|
|||||||
|
|
||||||
let image = images.choose(&mut rng()).ok_or(Error::NoAlbumFound)?;
|
let image = images.choose(&mut rng()).ok_or(Error::NoAlbumFound)?;
|
||||||
|
|
||||||
let actions = RandomConfig::get_random(db, ACTION_RANDOM_GROUP)?.unwrap_or_default();
|
picox.get_image_by_id(*image).await
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct Motivation {
|
pub struct Motivation {
|
||||||
pub image: Image,
|
|
||||||
pub action: String,
|
pub action: String,
|
||||||
pub goal: String,
|
pub goal: String,
|
||||||
pub border_color: String,
|
pub border_color: String,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user