diff --git a/platform/api/src/api/v1/gql/models/image_upload.rs b/platform/api/src/api/v1/gql/models/image_upload.rs index 0d0c61cf4..d67d73c49 100644 --- a/platform/api/src/api/v1/gql/models/image_upload.rs +++ b/platform/api/src/api/v1/gql/models/image_upload.rs @@ -23,7 +23,6 @@ pub struct ImageUpload { pub struct ImageUploadVariant { pub width: u32, pub height: u32, - pub scale: u32, pub url: String, pub format: ImageUploadFormat, pub byte_size: u32, @@ -76,7 +75,6 @@ impl From for Ima Self { width: value.width, height: value.height, - scale: value.scale, format: value.format().into(), byte_size: value.byte_size, url: value.path, diff --git a/platform/api/src/api/v1/upload/profile_picture.rs b/platform/api/src/api/v1/upload/profile_picture.rs index 7ff9dcdb5..0790f0c07 100644 --- a/platform/api/src/api/v1/upload/profile_picture.rs +++ b/platform/api/src/api/v1/upload/profile_picture.rs @@ -23,8 +23,11 @@ use crate::global::ApiGlobal; fn create_task(file_id: Ulid, input_path: &str, config: &ImageUploaderConfig, owner_id: Ulid) -> image_processor::Task { image_processor::Task { input_path: input_path.to_string(), - base_height: 128, // 128, 256, 384, 512 - base_width: 128, // 128, 256, 384, 512 + aspect_ratio: Some(image_processor::task::Ratio { + numerator: 1, + denominator: 1, + }), + clamp_aspect_ratio: true, formats: vec![ ImageFormat::PngStatic as i32, ImageFormat::AvifStatic as i32, @@ -42,8 +45,14 @@ fn create_task(file_id: Ulid, input_path: &str, config: &ImageUploaderConfig, ow max_processing_time_ms: 60 * 1000, // 60 seconds }), resize_algorithm: image_processor::task::ResizeAlgorithm::Lanczos3 as i32, - upscale: true, // For profile pictures we want to have a consistent size - scales: vec![1, 2, 3, 4], + upscale: image_processor::task::Upscale::NoPreserveSource as i32, + input_image_scaling: true, + scales: vec![ + 64, + 128, + 256, + 384, + ], resize_method: image_processor::task::ResizeMethod::PadCenter as i32, output_prefix: format!("{owner_id}/{file_id}"), } diff --git a/platform/api/src/igdb_cron.rs b/platform/api/src/igdb_cron.rs index ac126fb9b..d054eff76 100644 --- a/platform/api/src/igdb_cron.rs +++ b/platform/api/src/igdb_cron.rs @@ -587,9 +587,13 @@ fn create_task( ) -> image_processor::Task { image_processor::Task { callback_subject: config.callback_subject.clone(), - upscale: false, + upscale: image_processor::task::Upscale::NoPreserveSource as i32, output_prefix: format!("categories/{category_id}/{id}"), - scales: vec![1], + scales: vec![ + 720, + 1080, + ], + input_image_scaling: true, limits: Some(image_processor::task::Limits { max_processing_time_ms: 60000, ..Default::default() @@ -601,7 +605,11 @@ fn create_task( ], input_path: path, resize_method: image_processor::task::ResizeMethod::Fit as i32, + clamp_aspect_ratio: false, + aspect_ratio: Some(image_processor::task::Ratio { + numerator: 1, + denominator: 1, + }), resize_algorithm: image_processor::task::ResizeAlgorithm::Lanczos3 as i32, - ..Default::default() } } diff --git a/platform/api/src/main.rs b/platform/api/src/main.rs index 5a88db83f..a08d8001f 100644 --- a/platform/api/src/main.rs +++ b/platform/api/src/main.rs @@ -104,7 +104,7 @@ struct GlobalState { video_room_client: VideoRoomClient, video_playback_session_client: VideoPlaybackSessionClient, video_events_client: VideoEventsClient, - + redis: Arc, playback_private_key: Option>, @@ -330,7 +330,7 @@ pub async fn main() { r = api_future => r.context("api server stopped unexpectedly")?, r = subscription_manager => r.context("subscription manager stopped unexpectedly")?, r = video_event_handler => r.context("video event handler stopped unexpectedly")?, - r = image_upload_callback => r.context("image upload callback handler stopped unexpectedly")?, + r = image_upload_callback => r.context("image processor callback handler stopped unexpectedly")?, r = igdb_cron => r.context("igdb cron stopped unexpectedly")?, } diff --git a/platform/image_processor/src/processor/job/mod.rs b/platform/image_processor/src/processor/job/mod.rs index c0682e14c..75abc2589 100644 --- a/platform/image_processor/src/processor/job/mod.rs +++ b/platform/image_processor/src/processor/job/mod.rs @@ -28,6 +28,7 @@ pub(crate) mod libwebp; pub(crate) mod process; pub(crate) mod resize; pub(crate) mod smart_object; +pub(crate) mod scaling; pub(crate) struct Job<'a, G: ImageProcessorGlobal> { pub(crate) global: &'a Arc, @@ -230,11 +231,10 @@ impl<'a, G: ImageProcessorGlobal> Job<'a, G> { .iter() .map(|image| pb::scuffle::platform::internal::types::ProcessedImageVariant { path: image.url(&self.job.task.output_prefix), - format: image.request.1.into(), + format: image.request.into(), width: image.width as u32, height: image.height as u32, byte_size: image.data.len() as u32, - scale: image.request.0 as u32, }) .collect(), }, diff --git a/platform/image_processor/src/processor/job/process.rs b/platform/image_processor/src/processor/job/process.rs index e0548f695..c1b92f01f 100644 --- a/platform/image_processor/src/processor/job/process.rs +++ b/platform/image_processor/src/processor/job/process.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use bytes::Bytes; +use pb::scuffle::platform::internal::image_processor::task; use pb::scuffle::platform::internal::types::ImageFormat; use rgb::ComponentBytes; use sha2::Digest; @@ -11,6 +12,7 @@ use super::encoder::{AnyEncoder, Encoder, EncoderFrontend, EncoderSettings}; use super::resize::{ImageResizer, ImageResizerTarget}; use crate::database::Job; use crate::processor::error::{ProcessorError, Result}; +use crate::processor::job::scaling::{ScalingOptions, Ratio}; #[derive(Debug)] pub struct Image { @@ -21,12 +23,12 @@ pub struct Image { pub encoder: EncoderFrontend, pub data: Bytes, pub loop_count: LoopCount, - pub request: (usize, ImageFormat), + pub request: ImageFormat, } impl Image { pub fn file_extension(&self) -> &'static str { - match self.request.1 { + match self.request { ImageFormat::Avif | ImageFormat::AvifStatic => "avif", ImageFormat::Webp | ImageFormat::WebpStatic => "webp", ImageFormat::Gif => "gif", @@ -35,7 +37,7 @@ impl Image { } pub fn content_type(&self) -> &'static str { - match self.request.1 { + match self.request { ImageFormat::Avif | ImageFormat::AvifStatic => "image/avif", ImageFormat::Webp | ImageFormat::WebpStatic => "image/webp", ImageFormat::Gif => "image/gif", @@ -45,18 +47,19 @@ impl Image { pub fn is_static(&self) -> bool { matches!( - self.request.1, + self.request, ImageFormat::AvifStatic | ImageFormat::WebpStatic | ImageFormat::PngStatic ) } pub fn url(&self, prefix: &str) -> String { format!( - "{}/{}{}x.{}", - prefix.trim_end_matches('/'), - self.is_static().then_some("static_").unwrap_or_default(), - self.request.0, - self.file_extension() + "{prefix}/{static_prefix}{width}x{height}.{ext}", + prefix = prefix.trim_end_matches('/'), + static_prefix = self.is_static().then_some("static_").unwrap_or_default(), + width = self.width, + height = self.height, + ext = self.file_extension() ) } } @@ -72,7 +75,10 @@ pub fn process_job(backend: DecoderBackend, job: &Job, data: Cow<'_, [u8]>) -> R let info = decoder.info(); let formats = job.task.formats().collect::>(); - let scales = job.task.scales.iter().map(|s| *s as usize).collect::>(); + let mut scales = job.task.scales.iter().cloned().map(|s| s as usize).collect::>(); + + // Sorts the scales from smallest to largest. + scales.sort(); if formats.is_empty() || scales.is_empty() { tracing::debug!("no formats or scales specified"); @@ -118,32 +124,50 @@ pub fn process_job(backend: DecoderBackend, job: &Job, data: Cow<'_, [u8]>) -> R static_image: true, }; - let (base_width, base_height) = if job.task.upscale { - (job.task.base_width as f64, job.task.base_height as f64) - } else { - let largest_scale = scales.iter().max().copied().unwrap_or(1); - - let width = info.width as f64 / largest_scale as f64; - let height = info.height as f64 / largest_scale as f64; - - if width > job.task.base_width as f64 && height > job.task.base_height as f64 { - (job.task.base_width as f64, job.task.base_height as f64) - } else { - (width, height) - } + let (preserve_aspect_height, preserve_aspect_width) = match job.task.resize_method() { + task::ResizeMethod::Fit => (true, true), + task::ResizeMethod::Stretch => (false, false), + task::ResizeMethod::PadBottomLeft => (false, false), + task::ResizeMethod::PadBottomRight => (false, false), + task::ResizeMethod::PadTopLeft => (false, false), + task::ResizeMethod::PadTopRight => (false, false), + task::ResizeMethod::PadCenter => (false, false), + task::ResizeMethod::PadCenterLeft => (false, false), + task::ResizeMethod::PadCenterRight => (false, false), + task::ResizeMethod::PadTopCenter => (false, false), + task::ResizeMethod::PadBottomCenter => (false, false), + task::ResizeMethod::PadTop => (false, true), + task::ResizeMethod::PadBottom => (false, true), + task::ResizeMethod::PadLeft => (true, false), + task::ResizeMethod::PadRight => (true, false), }; - + + let upscale = job.task.upscale().into(); + + let scales = ScalingOptions { + input_height: info.height, + input_width: info.width, + input_image_scaling: job.task.input_image_scaling, + clamp_aspect_ratio: job.task.clamp_aspect_ratio, + scales, + aspect_ratio: job.task.aspect_ratio.as_ref().map(|r| Ratio::new(r.numerator as usize, r.denominator as usize)).unwrap_or(Ratio::ONE), + upscale, + preserve_aspect_height, + preserve_aspect_width, + }.compute(); + + // let base_width = input_width as f64 / job.task.aspect_width as f64; let mut resizers = scales .iter() .map(|scale| { ( - *scale, + scale.clone(), ImageResizer::new(ImageResizerTarget { - height: base_height.ceil() as usize * scale, - width: base_width.ceil() as usize * scale, + height: scale.height, + width: scale.width, algorithm: job.task.resize_algorithm(), method: job.task.resize_method(), - upscale: job.task.upscale, + upscale: upscale.is_yes(), }), Vec::with_capacity(info.frame_count), ) @@ -185,16 +209,14 @@ pub fn process_job(backend: DecoderBackend, job: &Job, data: Cow<'_, [u8]>) -> R drop(decoder); struct Stack { - scale: usize, static_encoders: Vec, animation_encoders: Vec, } let mut stacks = resizers - .iter_mut() - .map(|(scale, _, frames)| { + .iter() + .map(|(_, _, frames)| { Ok(Stack { - scale: *scale, static_encoders: static_formats .iter() .map(|&frontend| frontend.build(static_settings)) @@ -242,15 +264,12 @@ pub fn process_job(backend: DecoderBackend, job: &Job, data: Cow<'_, [u8]>) -> R encoder: info.frontend, data: output.into(), loop_count: info.loop_count, - request: ( - stack.scale, - match info.frontend { - EncoderFrontend::Gifski => ImageFormat::Gif, - EncoderFrontend::LibAvif => ImageFormat::Avif, - EncoderFrontend::LibWebp => ImageFormat::Webp, - EncoderFrontend::Png => unreachable!(), - }, - ), + request: match info.frontend { + EncoderFrontend::Gifski => ImageFormat::Gif, + EncoderFrontend::LibAvif => ImageFormat::Avif, + EncoderFrontend::LibWebp => ImageFormat::Webp, + EncoderFrontend::Png => unreachable!(), + }, }); } @@ -265,15 +284,12 @@ pub fn process_job(backend: DecoderBackend, job: &Job, data: Cow<'_, [u8]>) -> R encoder: info.frontend, data: output.into(), loop_count: info.loop_count, - request: ( - stack.scale, - match info.frontend { - EncoderFrontend::LibAvif => ImageFormat::AvifStatic, - EncoderFrontend::LibWebp => ImageFormat::WebpStatic, - EncoderFrontend::Png => ImageFormat::PngStatic, - EncoderFrontend::Gifski => unreachable!(), - }, - ), + request: match info.frontend { + EncoderFrontend::LibAvif => ImageFormat::AvifStatic, + EncoderFrontend::LibWebp => ImageFormat::WebpStatic, + EncoderFrontend::Png => ImageFormat::PngStatic, + EncoderFrontend::Gifski => unreachable!(), + }, }); } } diff --git a/platform/image_processor/src/processor/job/resize.rs b/platform/image_processor/src/processor/job/resize.rs index 77d1b8b45..64a70832a 100644 --- a/platform/image_processor/src/processor/job/resize.rs +++ b/platform/image_processor/src/processor/job/resize.rs @@ -50,7 +50,7 @@ impl ImageResizer { pub fn resize(&mut self, frame: &Frame) -> Result { let _abort_guard = utils::task::AbortGuard::new(); - let (width, height) = if self.target.method == ResizeMethod::Exact { + let (width, height) = if self.target.method == ResizeMethod::Stretch { (self.target.width, self.target.height) } else { let (mut width, mut height) = if frame.image.width() > frame.image.height() { @@ -117,7 +117,7 @@ impl ImageResizer { ResizeMethod::PadBottom => (0, height_delta, 0, 0), ResizeMethod::PadLeft => (0, 0, width_delta, 0), ResizeMethod::PadRight => (0, 0, 0, width_delta), - ResizeMethod::Exact => unreachable!(), + ResizeMethod::Stretch => unreachable!(), ResizeMethod::Fit => unreachable!(), }; diff --git a/platform/image_processor/src/processor/job/scaling.rs b/platform/image_processor/src/processor/job/scaling.rs new file mode 100644 index 000000000..e0e3c99cc --- /dev/null +++ b/platform/image_processor/src/processor/job/scaling.rs @@ -0,0 +1,584 @@ +use std::ops::MulAssign; + +#[derive(Debug, Clone)] +pub struct ScalingOptions { + pub input_width: usize, + pub input_height: usize, + pub aspect_ratio: Ratio, + pub clamp_aspect_ratio: bool, + pub preserve_aspect_width: bool, + pub preserve_aspect_height: bool, + pub upscale: Upscale, + pub input_image_scaling: bool, + pub scales: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Upscale { + Yes, + No, + NoPreserveSource, +} + +impl From for Upscale { + fn from(value: pb::scuffle::platform::internal::image_processor::task::Upscale) -> Self { + match value { + pb::scuffle::platform::internal::image_processor::task::Upscale::Yes => Upscale::Yes, + pb::scuffle::platform::internal::image_processor::task::Upscale::No => Upscale::No, + pb::scuffle::platform::internal::image_processor::task::Upscale::NoPreserveSource => Upscale::NoPreserveSource, + } + } +} + +impl Upscale { + pub fn is_yes(&self) -> bool { + matches!(self, Upscale::Yes) + } + + pub fn is_no(&self) -> bool { + matches!(self, Upscale::No | Upscale::NoPreserveSource) + } + + pub fn preserve_source(&self) -> bool { + matches!(self, Upscale::NoPreserveSource) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Size { + pub width: T, + pub height: T, +} + +#[derive(Debug, Clone, Copy)] +pub struct Ratio { + n: usize, + d: usize, +} + +impl Ratio { + pub const ONE: Self = Self::new(1, 1); + + pub const fn new(n: usize, d: usize) -> Self { + Self { n, d }.simplify() + } + + const fn gcd(&self) -> usize { + let mut a = self.n; + let mut b = self.d; + + while b != 0 { + let t = b; + b = a % b; + a = t; + } + + a + } + + const fn simplify(mut self) -> Self { + let gcd = self.gcd(); + + self.n /= gcd; + self.d /= gcd; + + self + } + + fn as_f64(&self) -> f64 { + self.n as f64 / self.d as f64 + } +} + +impl std::ops::Div for Ratio { + type Output = Ratio; + + fn div(self, rhs: usize) -> Self::Output { + Self { + n: self.n, + d: self.d * rhs, + }.simplify() + } +} + +impl std::ops::Mul for Ratio { + type Output = Ratio; + + fn mul(self, rhs: usize) -> Self::Output { + Self { + n: self.n * rhs, + d: self.d, + }.simplify() + } +} + +impl std::ops::Div for Ratio { + type Output = Ratio; + + fn div(self, rhs: Ratio) -> Self::Output { + Self { + n: self.n * rhs.d, + d: self.d * rhs.n, + }.simplify() + } +} + +impl std::ops::Mul for Ratio { + type Output = Ratio; + + fn mul(self, rhs: Ratio) -> Self::Output { + Self { + n: self.n * rhs.n, + d: self.d * rhs.d, + }.simplify() + } +} + +impl PartialEq for Ratio { + fn eq(&self, other: &Self) -> bool { + let this = self.simplify(); + let other = other.simplify(); + + this.n == other.n && this.d == other.d + } +} + +impl Eq for Ratio {} + +impl PartialOrd for Ratio { + fn partial_cmp(&self, other: &Self) -> Option { + let this = self.simplify(); + let other = other.simplify(); + + Some((this.n * other.d).cmp(&(this.d * other.n))) + } +} + +impl Ord for Ratio { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let this = self.simplify(); + let other = other.simplify(); + + (this.n * other.d).cmp(&(this.d * other.n)) + } +} + +impl MulAssign for Ratio { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl std::ops::DivAssign for Ratio { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +impl std::ops::Div for Size +where + T: std::ops::Div + Copy, +{ + type Output = Self; + + fn div(self, rhs: T) -> Self::Output { + Self { + width: self.width / rhs, + height: self.height / rhs, + } + } +} + +impl std::ops::Mul for Size +where + T: std::ops::Mul + Copy, +{ + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + Self { + width: self.width * rhs, + height: self.height * rhs, + } + } +} + +impl ScalingOptions { + pub fn compute(&mut self) -> Vec> { + // Sorts the scales from smallest to largest. + self.scales.sort_by(|a, b| a.partial_cmp(&b).unwrap()); + + let mut scales = self.compute_scales(); + let padded_size = self.padded_size(); + + let (best_idx, input_scale_factor) = scales.iter().position(|(size, _)| { + size.width >= padded_size.width || size.height >= padded_size.height + }).map(|idx| (idx, Ratio::ONE)).unwrap_or_else(|| { + let size = scales.last().unwrap().0; + + // Since its the padded size, the aspect ratio is the same as the target aspect ratio. + let input_scale_factor = padded_size.width / size.width; + + (scales.len() - 1, input_scale_factor) + }); + + dbg!(&scales); + + if self.input_image_scaling { + let scaled_width = padded_size.width / scales[best_idx].1 / input_scale_factor; + let scaled_height = padded_size.height / scales[best_idx].1 / input_scale_factor; + scales.iter_mut().for_each(|(size, scale)| { + size.width = *scale * scaled_width; + size.height = *scale * scaled_height; + }); + }; + + + if self.upscale.preserve_source() { + let padded_size = padded_size / input_scale_factor; + + dbg!(&padded_size); + + let size = scales[best_idx].0; + + if size.width > padded_size.width || size.height > padded_size.height { + scales[best_idx].0 = padded_size; + } + } + + if self.clamp_aspect_ratio { + scales.iter_mut().for_each(|(size, scale)| { + let scale = *scale; + + if self.aspect_ratio < Ratio::ONE && size.height > scale / self.aspect_ratio { + let height = scale / self.aspect_ratio; + size.width *= height / size.height; + size.height = height; + } else if self.aspect_ratio > Ratio::ONE && size.width > scale * self.aspect_ratio { + let width = scale * self.aspect_ratio; + size.height *= width / size.width; + size.width = width; + } else if self.aspect_ratio == Ratio::ONE && size.width > scale { + size.height *= scale / size.width; + size.width = scale; + } else if self.aspect_ratio == Ratio::ONE && size.height > scale { + size.width *= scale / size.height; + size.height = scale; + } + + size.width = size.width.max(Ratio::ONE); + size.height = size.height.max(Ratio::ONE); + }); + } + + if self.upscale.is_no() { + scales.retain(|(size, _)| { + size.width <= padded_size.width && size.height <= padded_size.height + }); + } + + scales.into_iter().map(|(mut size, _)| { + let input_aspect_ratio = self.input_aspect_ratio(); + + if self.preserve_aspect_height && self.aspect_ratio <= Ratio::ONE { + let height = size.height * self.aspect_ratio / input_aspect_ratio; + // size.width *= size.height / height; + size.height = height; + } else if self.preserve_aspect_width && self.aspect_ratio >= Ratio::ONE { + let width = size.width * input_aspect_ratio / self.aspect_ratio; + // size.height *= size.width / width; + size.width = width; + } + + Size { + width: size.width.as_f64().round() as usize, + height: size.height.as_f64().round() as usize, + } + }).collect() + } + + fn compute_scales(&self) -> Vec<(Size, Ratio)> { + self.scales.iter().copied().map(|scale| { + let scale = Ratio::new(scale, 1); + + let (width, height) = if self.aspect_ratio > Ratio::ONE { + (scale * self.aspect_ratio, scale) + } else { + (scale, scale / self.aspect_ratio) + }; + + (Size { width, height }, scale) + }).collect() + } + + fn input_aspect_ratio(&self) -> Ratio { + Ratio { + n: self.input_width, + d: self.input_height, + }.simplify() + } + + fn padded_size(&self) -> Size { + let width = Ratio::new(self.input_width, 1); + let height = Ratio::new(self.input_height, 1); + + let (width, height) = if self.aspect_ratio < Ratio::ONE { + (width, width / self.aspect_ratio) + } else { + (height * self.aspect_ratio, height) + }; + + Size { width, height } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compute_scales_same_aspect() { + let mut options = ScalingOptions { + input_width: 100, + input_height: 100, + aspect_ratio: Ratio::new(1, 1), + preserve_aspect_width: false, + preserve_aspect_height: false, + upscale: Upscale::Yes, + input_image_scaling: false, + clamp_aspect_ratio: true, + scales: vec![ + 32, + 64, + 96, + 128, + ], + }; + + assert_eq!(options.compute(), vec![ + Size { width: 32, height: 32 }, + Size { width: 64, height: 64 }, + Size { width: 96, height: 96 }, + Size { width: 128, height: 128 }, + ]); + + options.upscale = Upscale::No; + + assert_eq!(options.compute(), vec![ + Size { width: 32, height: 32 }, + Size { width: 64, height: 64 }, + Size { width: 96, height: 96 }, + ]); + + options.upscale = Upscale::NoPreserveSource; + + assert_eq!(options.compute(), vec![ + Size { width: 32, height: 32 }, + Size { width: 64, height: 64 }, + Size { width: 96, height: 96 }, + Size { width: 100, height: 100 }, + ]); + + options.input_height = 112; + options.input_width = 112; + options.input_image_scaling = true; + options.upscale = Upscale::No; + + assert_eq!(options.compute(), vec![ + Size { width: 28, height: 28 }, + Size { width: 56, height: 56 }, + Size { width: 84, height: 84 }, + Size { width: 112, height: 112 }, + ]); + } + + #[test] + fn test_compute_scales_different_aspect() { + let mut options = ScalingOptions { + input_width: 100, + input_height: 100, + aspect_ratio: Ratio::new(16, 9), + preserve_aspect_width: false, + preserve_aspect_height: false, + upscale: Upscale::Yes, + input_image_scaling: false, + clamp_aspect_ratio: true, + scales: vec![ + 360, + 720, + 1080, + ], + }; + + assert_eq!(options.compute(), vec![ + Size { width: 640, height: 360 }, + Size { width: 1280, height: 720 }, + Size { width: 1920, height: 1080 }, + ]); + + options.upscale = Upscale::No; + assert_eq!(options.compute(), vec![]); + + options.upscale = Upscale::NoPreserveSource; + assert_eq!(options.compute(), vec![ + Size { width: 178, height: 100 }, + ]); + + options.aspect_ratio = Ratio::new(9, 16); + options.upscale = Upscale::Yes; + + assert_eq!(options.compute(), vec![ + Size { width: 360, height: 640 }, + Size { width: 720, height: 1280 }, + Size { width: 1080, height: 1920 }, + ]); + + options.upscale = Upscale::No; + assert_eq!(options.compute(), vec![]); + + options.upscale = Upscale::NoPreserveSource; + assert_eq!(options.compute(), vec![ + Size { width: 100, height: 178 }, + ]); + + options.input_width = 1920; + options.input_height = 1080; + options.upscale = Upscale::Yes; + + assert_eq!(options.compute(), vec![ + Size { width: 360, height: 640 }, + Size { width: 720, height: 1280 }, + Size { width: 1080, height: 1920 }, + ]); + + options.upscale = Upscale::No; + assert_eq!(options.compute(), vec![ + Size { width: 360, height: 640 }, + Size { width: 720, height: 1280 }, + Size { width: 1080, height: 1920 }, + ]); + + options.upscale = Upscale::NoPreserveSource; + assert_eq!(options.compute(), vec![ + Size { width: 360, height: 640 }, + Size { width: 720, height: 1280 }, + Size { width: 1080, height: 1920 }, + ]); + } + + #[test] + fn test_compute_scales_image_scaling() { + let mut options = ScalingOptions { + input_width: 112, + input_height: 112, + aspect_ratio: Ratio::new(3, 1), + preserve_aspect_width: true, + preserve_aspect_height: true, + upscale: Upscale::NoPreserveSource, + input_image_scaling: true, + clamp_aspect_ratio: true, + scales: vec![ + 32, + 64, + 96, + 128, + ], + }; + + assert_eq!(options.compute(), vec![ + Size { width: 28, height: 28 }, + Size { width: 56, height: 56 }, + Size { width: 84, height: 84 }, + Size { width: 112, height: 112 }, + ]); + + options.input_width = 112 * 2; + assert_eq!(options.compute(), vec![ + Size { width: 28 * 2, height: 28 }, + Size { width: 56 * 2, height: 56 }, + Size { width: 84 * 2, height: 84 }, + Size { width: 112 * 2, height: 112 }, + ]); + + options.input_width = 112 * 3; + assert_eq!(options.compute(), vec![ + Size { width: 28 * 3, height: 28 }, + Size { width: 56 * 3, height: 56 }, + Size { width: 84 * 3, height: 84 }, + Size { width: 112 * 3, height: 112 }, + ]); + + options.input_width = 112 * 4; + assert_eq!(options.compute(), vec![ + Size { width: 32 * 3, height: 24 }, + Size { width: 64 * 3, height: 48 }, + Size { width: 96 * 3, height: 72 }, + Size { width: 128 * 3, height: 96 }, + ]); + + options.input_width = 112 / 2; + assert_eq!(options.compute(), vec![ + Size { width: 28 / 2, height: 28 }, + Size { width: 56 / 2, height: 56 }, + Size { width: 84 / 2, height: 84 }, + Size { width: 112 / 2, height: 112 }, + ]); + + options.input_width = 112 / 3; + assert_eq!(options.compute(), vec![ + Size { width: 9, height: 28 }, + Size { width: 19, height: 56 }, + Size { width: 28, height: 84 }, + Size { width: 37, height: 112 }, + ]); + } + + #[test] + fn test_compute_scales_any_scale() { + let mut options = ScalingOptions { + input_width: 245, + input_height: 1239, + aspect_ratio: Ratio::new(1, 1), + preserve_aspect_width: true, + preserve_aspect_height: true, + upscale: Upscale::NoPreserveSource, + input_image_scaling: true, + clamp_aspect_ratio: true, + scales: vec![ + 720, + 1080, + ], + }; + + assert_eq!(options.compute(), vec![ + Size { width: 142, height: 720 }, + Size { width: 214, height: 1080 }, + ]); + + options.input_width = 1239; + options.input_height = 245; + + assert_eq!(options.compute(), vec![ + Size { width: 720, height: 142 }, + Size { width: 1080, height: 214 }, + ]); + + options.clamp_aspect_ratio = false; + options.input_image_scaling = false; + options.input_height = 1239; + options.input_width = 245; + + assert_eq!(options.compute(), vec![ + Size { width: 142, height: 720 }, + Size { width: 214, height: 1080 }, + ]); + + options.input_height = 245; + options.input_width = 1239; + + assert_eq!(options.compute(), vec![ + Size { width: 720, height: 142 }, + Size { width: 1080, height: 214 }, + ]); + } +} diff --git a/proto/scuffle/platform/internal/image_processor.proto b/proto/scuffle/platform/internal/image_processor.proto index 7356908b5..87ce9a8b9 100644 --- a/proto/scuffle/platform/internal/image_processor.proto +++ b/proto/scuffle/platform/internal/image_processor.proto @@ -7,7 +7,7 @@ import "scuffle/platform/internal/types/image_format.proto"; message Task { enum ResizeMethod { Fit = 0; - Exact = 1; + Stretch = 1; PadBottomLeft = 2; PadBottomRight = 3; PadTopLeft = 4; @@ -35,17 +35,30 @@ message Task { string input_path = 1; - uint32 base_width = 2; - uint32 base_height = 3; + message Ratio { + uint32 numerator = 1; + uint32 denominator = 2; + } + + Ratio aspect_ratio = 2; + bool clamp_aspect_ratio = 3; + + enum Upscale { + Yes = 0; + No = 1; + NoPreserveSource = 2; + } - repeated scuffle.platform.internal.types.ImageFormat formats = 4; - ResizeMethod resize_method = 5; - ResizeAlgorithm resize_algorithm = 6; - repeated uint32 scales = 7; + Upscale upscale = 4; - bool upscale = 8; + repeated scuffle.platform.internal.types.ImageFormat formats = 5; + ResizeMethod resize_method = 6; + ResizeAlgorithm resize_algorithm = 7; + + bool input_image_scaling = 8; + repeated uint32 scales = 9; - string output_prefix = 9; + string output_prefix = 10; message Limits { uint32 max_processing_time_ms = 1; @@ -55,7 +68,7 @@ message Task { uint32 max_input_duration_ms = 5; } - optional Limits limits = 10; + optional Limits limits = 11; - string callback_subject = 11; + string callback_subject = 12; } diff --git a/proto/scuffle/platform/internal/types/processed_image_variant.proto b/proto/scuffle/platform/internal/types/processed_image_variant.proto index d522251b5..d234a0133 100644 --- a/proto/scuffle/platform/internal/types/processed_image_variant.proto +++ b/proto/scuffle/platform/internal/types/processed_image_variant.proto @@ -8,7 +8,6 @@ message ProcessedImageVariant { uint32 width = 1; uint32 height = 2; ImageFormat format = 3; - uint32 scale = 4; - uint32 byte_size = 5; - string path = 6; + uint32 byte_size = 4; + string path = 5; }