Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion example.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"enable_images": true,
"imgur_images": true,
"litterbox_images": false,
"process_images": true
"process_images": true,
"size": 512,
"bg": true,
"bg_blur": 3,
"corner_radius": 4
}
}
36 changes: 36 additions & 0 deletions jellyfin-rpc-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ pub struct Images {
pub litterbox_images: bool,
/// Processes images by making them square and adding a blur.
pub process_images: bool,
/// The size of the output square image canvas (e.g., 512 for 512x512px).
pub size: Option<u32>,
/// Whether to create a blurred background. Default: true.
pub bg: bool,
/// The blur radius for the background image as a percentage of canvas size.
/// Default: 3.
pub bg_blur: f32,
/// Corner radius as a percentage of the image size.
/// Only applied when background is disabled. Default: 4.
pub corner_radius: Option<f32>,
}

impl Config {
Expand Down Expand Up @@ -164,6 +174,16 @@ pub struct ImagesBuilder {
pub imgur_images: Option<bool>,
pub litterbox_images: Option<bool>,
pub process_images: Option<bool>,
/// The size of the output square image canvas (e.g., 512 for 512x512px).
pub size: Option<u32>,
/// Whether to create a blurred background. Default: true.
pub bg: Option<bool>,
/// The blur radius for the background image as a percentage of canvas size.
/// Default: 3.
pub bg_blur: Option<f32>,
/// Corner radius as a percentage of the image size.
/// Only applied when background is disabled. Default: 4.
pub corner_radius: Option<f32>,
}

/// Find urls.json in filesystem, used to store images that were already previously uploaded to imgur.
Expand Down Expand Up @@ -368,17 +388,29 @@ impl ConfigBuilder {
let imgur_images;
let litterbox_images;
let process_images;
let image_size;
let image_bg;
let image_bg_blur;
let image_corner_radius;

if let Some(images) = self.images {
enable_images = images.enable_images.unwrap_or(false);
imgur_images = images.imgur_images.unwrap_or(false);
litterbox_images = images.litterbox_images.unwrap_or(false);
process_images = images.process_images.unwrap_or(true);
image_size = images.size;
image_bg = images.bg.unwrap_or(true);
image_bg_blur = images.bg_blur.unwrap_or(3.0);
image_corner_radius = images.corner_radius.or(Some(4.0));
} else {
enable_images = false;
imgur_images = false;
litterbox_images = false;
process_images = true;
image_size = None;
image_bg = true;
image_bg_blur = 3.0;
image_corner_radius = Some(4.0);
}

let url;
Expand Down Expand Up @@ -429,6 +461,10 @@ impl ConfigBuilder {
imgur_images,
litterbox_images,
process_images,
size: image_size,
bg: image_bg,
bg_blur: image_bg_blur,
corner_radius: image_corner_radius,
},
}
}
Expand Down
4 changes: 4 additions & 0 deletions jellyfin-rpc-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
.use_imgur(conf.images.imgur_images)
.use_litterbox(conf.images.litterbox_images)
.process_images(conf.images.process_images)
.image_size(conf.images.size)

Check failure on line 104 in jellyfin-rpc-cli/src/main.rs

View workflow job for this annotation

GitHub Actions / ubuntu-arm32

no method named `image_size` found for mutable reference `&mut jellyfin_rpc::ClientBuilder` in the current scope

Check failure on line 104 in jellyfin-rpc-cli/src/main.rs

View workflow job for this annotation

GitHub Actions / ubuntu

no method named `image_size` found for mutable reference `&mut jellyfin_rpc::ClientBuilder` in the current scope

Check failure on line 104 in jellyfin-rpc-cli/src/main.rs

View workflow job for this annotation

GitHub Actions / windows

no method named `image_size` found for mutable reference `&mut jellyfin_rpc::ClientBuilder` in the current scope

Check failure on line 104 in jellyfin-rpc-cli/src/main.rs

View workflow job for this annotation

GitHub Actions / macos-arm64

no method named `image_size` found for mutable reference `&mut jellyfin_rpc::ClientBuilder` in the current scope

Check failure on line 104 in jellyfin-rpc-cli/src/main.rs

View workflow job for this annotation

GitHub Actions / macos-x86_64

no method named `image_size` found for mutable reference `&mut jellyfin_rpc::ClientBuilder` in the current scope

Check failure on line 104 in jellyfin-rpc-cli/src/main.rs

View workflow job for this annotation

GitHub Actions / ubuntu-arm64

no method named `image_size` found for mutable reference `&mut jellyfin_rpc::ClientBuilder` in the current scope
.image_background(conf.images.bg)
.image_background_blur(conf.images.bg_blur)
.image_corner_radius(conf.images.corner_radius)
.large_image_text(format!("Jellyfin-RPC v{}", VERSION.unwrap_or("UNKNOWN")))
.imgur_urls_file_location(args.image_urls.clone().unwrap_or(get_urls_path()?))
.litterbox_urls_file_location(args.image_urls.unwrap_or(get_urls_path()?));
Expand Down
95 changes: 84 additions & 11 deletions jellyfin-rpc/src/external/image_utils.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,102 @@
use image::{DynamicImage, GenericImageView, ImageFormat, imageops};
use std::io::{Cursor};

pub fn make_square_with_blur(input_bytes: &[u8]) -> Result<Vec<u8>, image::ImageError> {
#[derive(Debug, Clone)]
pub struct ImageProcessingOptions {
pub size: Option<u32>,
pub background: bool,
pub background_blur: f32,
pub corner_radius: Option<f32>,
}

impl Default for ImageProcessingOptions {
fn default() -> Self {
Self {
size: None,
background: true,
background_blur: 3.0,
corner_radius: Some(4.0),
}
}
}

pub fn make_square_with_blur(input_bytes: &[u8], options: &ImageProcessingOptions) -> Result<Vec<u8>, image::ImageError> {
let img = image::load_from_memory(input_bytes)?;
let (width, height) = img.dimensions();
let size = width.max(height);

let bg_buf = imageops::resize(&img, size, size, imageops::FilterType::Gaussian);
let mut bg_dyn = DynamicImage::ImageRgba8(bg_buf);
bg_dyn = DynamicImage::ImageRgba8(imageops::blur(&bg_dyn, 20.0));
let size = options.size.unwrap_or_else(|| width.max(height));

let fg_buf = if width > height {
imageops::resize(&img, size, (size as f32 * (height as f32 / width as f32)) as u32, imageops::FilterType::Lanczos3)
let new_height = (size as f32 * (height as f32 / width as f32)) as u32;
imageops::resize(&img, size, new_height, imageops::FilterType::Lanczos3)
} else {
imageops::resize(&img, (size as f32 * (width as f32 / height as f32)) as u32, size, imageops::FilterType::Lanczos3)
let new_width = (size as f32 * (width as f32 / height as f32)) as u32;
imageops::resize(&img, new_width, size, imageops::FilterType::Lanczos3)
};

let fg_dyn = DynamicImage::ImageRgba8(fg_buf);
let (fg_w, fg_h) = fg_dyn.dimensions();

let mut canvas = DynamicImage::new_rgba8(size, size);
imageops::overlay(&mut canvas, &bg_dyn, 0, 0);
imageops::overlay(&mut canvas, &fg_dyn, ((size - fg_w) / 2) as i64, ((size - fg_h) / 2) as i64);

if options.background {
let bg_buf = imageops::resize(&img, size, size, imageops::FilterType::Gaussian);
let mut bg_dyn = DynamicImage::ImageRgba8(bg_buf);
if options.background_blur > 0.0 {
let blur_radius = (size as f32) * (options.background_blur / 100.0);
bg_dyn = DynamicImage::ImageRgba8(imageops::blur(&bg_dyn, blur_radius));
}
imageops::overlay(&mut canvas, &bg_dyn, 0, 0);
imageops::overlay(&mut canvas, &fg_dyn, ((size - fg_w) / 2) as i64, ((size - fg_h) / 2) as i64);
} else {
let mut fg_rounded = fg_dyn;

if let Some(radius_percent) = options.corner_radius {
let radius = ((size as f32) * (radius_percent / 100.0)) as u32;
if radius > 0 {
apply_rounded_corners(&mut fg_rounded, radius);
}
}

imageops::overlay(&mut canvas, &fg_rounded, ((size - fg_w) / 2) as i64, ((size - fg_h) / 2) as i64);
}

let mut buf = Vec::new();
canvas.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)?;
Ok(buf)
}

fn apply_rounded_corners(img: &mut DynamicImage, radius: u32) {
let (width, height) = img.dimensions();
let rgba = img.as_mut_rgba8().unwrap();
let radius_f = radius as f32;

for y in 0..height {
for x in 0..width {
let corner_center = if x < radius && y < radius {
Some((radius - 1, radius - 1))
} else if x >= width - radius && y < radius {
Some((width - radius, radius - 1))
} else if x < radius && y >= height - radius {
Some((radius - 1, height - radius))
} else if x >= width - radius && y >= height - radius {
Some((width - radius, height - radius))
} else {
None
};

if let Some((cx, cy)) = corner_center {
let dx = x as f32 - cx as f32;
let dy = y as f32 - cy as f32;
let distance = (dx * dx + dy * dy).sqrt();

if distance > radius_f {
let pixel = rgba.get_pixel_mut(x, y);
pixel[3] = 0;
} else if distance > radius_f - 1.0 {
let alpha_factor = radius_f - distance;
let pixel = rgba.get_pixel_mut(x, y);
pixel[3] = (pixel[3] as f32 * alpha_factor).round() as u8;
}
}
}
}
}
2 changes: 1 addition & 1 deletion jellyfin-rpc/src/external/imgur.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ fn upload(client: &Client) -> JfResult<Url> {

let body = if client.process_images {
use crate::external::image_utils::make_square_with_blur;
make_square_with_blur(&image_bytes)?
make_square_with_blur(&image_bytes, &client.image_processing_options)?
} else {
image_bytes.to_vec()
};
Expand Down
2 changes: 1 addition & 1 deletion jellyfin-rpc/src/external/litterbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ fn upload(client: &Client) -> JfResult<Url> {

let file_bytes = if client.process_images {
use crate::external::image_utils::make_square_with_blur;
make_square_with_blur(&image_bytes)?
make_square_with_blur(&image_bytes, &client.image_processing_options)?
} else {
image_bytes.to_vec()
};
Expand Down
43 changes: 43 additions & 0 deletions jellyfin-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::time::SystemTime;
use url::Url;
pub use external::image_utils::ImageProcessingOptions;

mod error;
mod external;
Expand Down Expand Up @@ -42,6 +43,7 @@ pub struct Client {
imgur_options: ImgurOptions,
litterbox_options: LitterboxOptions,
process_images: bool,
image_processing_options: external::image_utils::ImageProcessingOptions,
large_image_text: String,
}

Expand Down Expand Up @@ -1013,6 +1015,10 @@ pub struct ClientBuilder {
litterbox_urls_file_location: String,
large_image_text: String,
process_images: bool,
image_size: Option<u32>,
image_background: bool,
image_background_blur: f32,
image_corner_radius: Option<f32>,
}

impl ClientBuilder {
Expand All @@ -1032,6 +1038,9 @@ impl ClientBuilder {
}),
show_paused: true,
process_images: true,
image_background: true,
image_background_blur: 3.0,
image_corner_radius: Some(4.0),
..Default::default()
}
}
Expand Down Expand Up @@ -1266,6 +1275,34 @@ impl ClientBuilder {
self
}

/// Set the size of the processed image (e.g., 512 for 512x512px).
/// Default: uses original image's largest dimension.
pub fn image_size(&mut self, size: Option<u32>) -> &mut Self {
self.image_size = size;
self
}

/// Enable or disable the blurred background for processed images.
/// Defaults to `true`. When disabled the background is transparent.
pub fn image_background(&mut self, val: bool) -> &mut Self {
self.image_background = val;
self
}

/// Set the blur radius for the background image as a percentage of canvas size.
/// Valid range: 0-50. Defaults to `3.0`.
pub fn image_background_blur(&mut self, blur: f32) -> &mut Self {
self.image_background_blur = blur;
self
}

/// Set the rounded corner radius as a percentage of the image size.
/// Only applied when background is disabled. Defaults to `4.0`.
pub fn image_corner_radius(&mut self, radius: Option<f32>) -> &mut Self {
self.image_corner_radius = radius;
self
}

/// Text to be displayed when hovering the large activity image in Discord
///
/// Empty by default
Expand Down Expand Up @@ -1342,6 +1379,12 @@ impl ClientBuilder {
urls_location: self.litterbox_urls_file_location,
},
process_images: self.process_images,
image_processing_options: external::image_utils::ImageProcessingOptions {
size: self.image_size,
background: self.image_background,
background_blur: self.image_background_blur,
corner_radius: self.image_corner_radius,
},
large_image_text: self.large_image_text,
})
}
Expand Down
Loading