From d2076eac4c56f9303d2546ba35d29fc5f34dce8e Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Sun, 14 Jul 2024 01:05:22 +0000 Subject: [PATCH] feat: add image processor input metadata --- .../proto/scuffle/image_processor/types.proto | 43 ++++-- image-processor/src/management/mod.rs | 3 +- image-processor/src/management/validation.rs | 9 ++ .../src/worker/process/blocking.rs | 51 ++++--- .../src/worker/process/decoder/ffmpeg.rs | 20 ++- .../src/worker/process/decoder/libavif.rs | 5 +- .../src/worker/process/decoder/libwebp.rs | 13 +- .../src/worker/process/decoder/mod.rs | 19 ++- .../src/worker/process/encoder/gifski.rs | 4 +- .../src/worker/process/encoder/libavif.rs | 4 +- .../src/worker/process/encoder/libwebp.rs | 4 +- .../src/worker/process/encoder/mod.rs | 2 - .../src/worker/process/encoder/png.rs | 4 +- image-processor/src/worker/process/mod.rs | 135 +++++++++++------- 14 files changed, 208 insertions(+), 108 deletions(-) diff --git a/image-processor/proto/scuffle/image_processor/types.proto b/image-processor/proto/scuffle/image_processor/types.proto index 99a70597..2a67063d 100644 --- a/image-processor/proto/scuffle/image_processor/types.proto +++ b/image-processor/proto/scuffle/image_processor/types.proto @@ -26,6 +26,8 @@ message DrivePath { // Possible template argument values are: // - {id} - The id of the task. string path = 2; + // The acl to use for the drive. (used when uploading) + optional string acl = 3; } // The resize method determines how the image processor should resize the image. @@ -111,6 +113,8 @@ message InputMetadata { uint32 width = 3; // If this is different from the actual height the image processor will generate a fatal error. uint32 height = 4; + // The loop count of the input image. + int32 loop_count = 5; } // InputUpload is used to upload an image to a drive configured in the image processor config. @@ -229,33 +233,43 @@ message OutputFormatOptions { message OutputFile { // The path to the output file. - string path = 1; + DrivePath path = 1; // The content type of the output file. string content_type = 2; - // The acl of the output file. - optional string acl = 3; // Width of the output image. - uint32 width = 4; + uint32 width = 3; // Height of the output image. - uint32 height = 5; + uint32 height = 4; // The frame count of the output image. - uint32 frame_count = 6; + uint32 frame_count = 5; // The duration of the output image. - uint32 duration_ms = 7; + uint32 duration_ms = 6; // The size of the output image in bytes. - uint32 size = 8; + uint32 size = 7; // The format of the output image. - OutputFormat format = 9; + OutputFormat format = 8; + // Loop count of the output image. + int32 loop_count = 9; } // Returned after the image is processed. message InputFileMetadata { + // The final path of the input image. + DrivePath path = 1; + // The content type of the input image. + string content_type = 2; // The width of the input image. - uint32 width = 1; + uint32 width = 3; // The height of the input image. - uint32 height = 2; + uint32 height = 4; // The frame count of the input image. - uint32 frame_count = 3; + uint32 frame_count = 5; + // The duration of the input image. + uint32 duration_ms = 6; + // The size of the input image in bytes. + uint32 size = 7; + // The loop count of the input image. + int32 loop_count = 8; } message Output { @@ -274,9 +288,8 @@ message Output { // - {ext} - The extension of the output image. (e.g. 'webp', 'avif', etc.) DrivePath drive_path = 1; - // Override the acl of the output images. - // By default this will use the ACL specified by the output drive config. - optional string acl_override = 2; + // The path to the input image to re-upload. + optional DrivePath input_reupload_path = 2; // The desired format to encode the output image. repeated OutputFormatOptions formats = 3; diff --git a/image-processor/src/management/mod.rs b/image-processor/src/management/mod.rs index f1858122..3e3d3ba3 100644 --- a/image-processor/src/management/mod.rs +++ b/image-processor/src/management/mod.rs @@ -67,6 +67,7 @@ impl ManagementServer { let drive_path = DrivePath { drive: drive_path.drive, + acl: drive_path.acl, path: path.clone(), }; @@ -86,7 +87,7 @@ impl ManagementServer { &path, Bytes::from(input_upload.binary), Some(DriveWriteOptions { - acl: input_upload.acl, + acl: drive_path.acl.clone(), cache_control: input_upload.cache_control, content_disposition: input_upload.content_disposition, content_type: input_upload.content_type, diff --git a/image-processor/src/management/validation.rs b/image-processor/src/management/validation.rs index 5381d235..faeb79c8 100644 --- a/image-processor/src/management/validation.rs +++ b/image-processor/src/management/validation.rs @@ -262,6 +262,15 @@ pub fn validate_output(global: &Arc, mut fragment: Fragment, output: Opt ], )?; + if let Some(input_reupload_path) = output.input_reupload_path.as_ref() { + validate_drive_path( + global, + fragment.push("path"), + Some(input_reupload_path), + &["id", "width", "height", "ext"], + )?; + } + if output.formats.is_empty() { return Err(Error { code: ErrorCode::InvalidInput as i32, diff --git a/image-processor/src/worker/process/blocking.rs b/image-processor/src/worker/process/blocking.rs index ccb8dee5..c38967ab 100644 --- a/image-processor/src/worker/process/blocking.rs +++ b/image-processor/src/worker/process/blocking.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use bytes::Bytes; use file_format::FileFormat; -use scuffle_image_processor_proto::{animation_config, InputFileMetadata, Output, OutputFormat, OutputFormatOptions, Task}; +use scuffle_image_processor_proto::{animation_config, Output, OutputFormat, OutputFormatOptions, Task}; use tokio::sync::OwnedSemaphorePermit; use super::decoder::{AnyDecoder, Decoder, DecoderFrontend, DecoderInfo, LoopCount}; @@ -13,7 +13,7 @@ use super::resize::{ImageResizer, ResizeOutputTarget}; use super::JobError; pub struct JobOutput { - pub input: InputFileMetadata, + pub input: InputImage, pub output: Vec, } @@ -28,6 +28,16 @@ pub struct OutputImage { pub data: Vec, pub frame_count: usize, pub duration_ms: u64, + pub loop_count: LoopCount, +} + +pub struct InputImage { + pub format: FileFormat, + pub width: usize, + pub height: usize, + pub loop_count: LoopCount, + pub duration_ms: u64, + pub frame_count: usize, } #[derive(Clone, Copy)] @@ -59,11 +69,7 @@ impl Drop for CancelToken { } } -pub async fn spawn( - task: Task, - input: Bytes, - permit: Arc, -) -> Result<(DecoderInfo, JobOutput), JobError> { +pub async fn spawn(task: Task, input: Bytes, permit: Arc) -> Result { let cancel_token = CancelToken::new(); let _cancel_guard = cancel_token.clone(); @@ -86,9 +92,7 @@ pub async fn spawn( } } - let info = task.decoder_info.clone(); - - task.finish().map(|out| (info, out)) + task.finish() }) .await? } @@ -96,6 +100,7 @@ pub async fn spawn( struct BlockingTask<'a> { decoder: AnyDecoder<'a>, decoder_info: DecoderInfo, + input: InputImage, frame_configs: Vec>, resizer: ImageResizer, static_encoders: Vec<(usize, Vec<(ResizeOutputTarget, AnyEncoder)>)>, @@ -104,6 +109,7 @@ struct BlockingTask<'a> { frame_idx: usize, duration_carried_ms: f64, frame_rate_factor: Option, + loop_count: LoopCount, } fn split_formats(output: &Output) -> (Vec<(usize, &OutputFormatOptions)>, Vec<(usize, &OutputFormatOptions)>) { @@ -166,8 +172,8 @@ impl<'a> BlockingTask<'a> { return Err(JobError::InvalidJob); } - let file_format = DecoderFrontend::from_format(FileFormat::from_bytes(input))?; - let decoder = file_format.build(task, Cow::Borrowed(input))?; + let file_format = FileFormat::from_bytes(input); + let decoder = DecoderFrontend::from_format(file_format.clone())?.build(task, Cow::Borrowed(input))?; let decoder_info = decoder.info(); @@ -291,6 +297,15 @@ impl<'a> BlockingTask<'a> { static_frame_idx, frame_idx: 0, duration_carried_ms: 0.0, + input: InputImage { + format: file_format, + width: decoder_info.width, + height: decoder_info.height, + loop_count: decoder_info.loop_count, + duration_ms: 0, + frame_count: decoder_info.frame_count, + }, + loop_count, frame_rate_factor: anim_config.and_then(|config| match config.frame_rate.as_ref()? { animation_config::FrameRate::FrameRateFactor(factor) => Some(*factor), _ => None, @@ -362,7 +377,7 @@ impl<'a> BlockingTask<'a> { Ok(true) } - pub fn finish(self) -> Result { + pub fn finish(mut self) -> Result { let output = self .static_encoders .into_iter() @@ -381,17 +396,17 @@ impl<'a> BlockingTask<'a> { frame_count: info.frame_count, duration_ms: info.duration, data: encoder.finish()?, + loop_count: self.loop_count, }) }) }) .collect::>()?; + // Update the duration of the input. + self.input.duration_ms = u64::try_from(self.decoder.duration_ms()).unwrap_or(0); + Ok(JobOutput { - input: InputFileMetadata { - width: self.decoder_info.width as u32, - height: self.decoder_info.height as u32, - frame_count: self.decoder_info.frame_count as u32, - }, + input: self.input, output, }) } diff --git a/image-processor/src/worker/process/decoder/ffmpeg.rs b/image-processor/src/worker/process/decoder/ffmpeg.rs index 3928b492..9b218833 100644 --- a/image-processor/src/worker/process/decoder/ffmpeg.rs +++ b/image-processor/src/worker/process/decoder/ffmpeg.rs @@ -14,6 +14,7 @@ pub struct FfmpegDecoder<'data> { info: DecoderInfo, input_stream_index: i32, average_frame_duration: u64, + duration_ms: i64, previous_timestamp: Option, send_packet: bool, eof: bool, @@ -76,15 +77,20 @@ impl<'data> FfmpegDecoder<'data> { } } + let duration_ms = + (input_stream_duration * input_stream_time_base.num as i64 * 1000) / input_stream_time_base.den as i64; + + if duration_ms < 0 { + return Err(DecoderError::InvalidTimeBase); + } + if let Some(max_input_duration_ms) = task.limits.as_ref().and_then(|l| l.max_input_duration_ms) { // actual duration // = duration * (time_base.num / time_base.den) * 1000 // = (duration * time_base.num * 1000) / time_base.den - let duration = - (input_stream_duration * input_stream_time_base.num as i64 * 1000) / input_stream_time_base.den as i64; - if duration > max_input_duration_ms as i64 { - return Err(DecoderError::TooLong(duration)); + if duration_ms > max_input_duration_ms as i64 { + return Err(DecoderError::TooLong(duration_ms)); } } @@ -98,7 +104,6 @@ impl<'data> FfmpegDecoder<'data> { )?; let info = DecoderInfo { - decoder: DecoderFrontend::Ffmpeg, width: decoder.width() as usize, height: decoder.height() as usize, frame_count: input_stream_frames as usize, @@ -125,6 +130,7 @@ impl<'data> FfmpegDecoder<'data> { send_packet: true, frame, average_frame_duration, + duration_ms, previous_timestamp: Some(0), }) } @@ -221,6 +227,10 @@ impl Decoder for FfmpegDecoder<'_> { } } + fn duration_ms(&self) -> i64 { + self.duration_ms + } + fn info(&self) -> DecoderInfo { self.info } diff --git a/image-processor/src/worker/process/decoder/libavif.rs b/image-processor/src/worker/process/decoder/libavif.rs index d878d0c2..c1b4d1d7 100644 --- a/image-processor/src/worker/process/decoder/libavif.rs +++ b/image-processor/src/worker/process/decoder/libavif.rs @@ -55,7 +55,6 @@ impl<'data> AvifDecoder<'data> { let image = AvifRgbImage::new(decoder.as_ref()); let info = DecoderInfo { - decoder: DecoderFrontend::LibAvif, width: image.width as usize, height: image.height as usize, loop_count: if decoder.as_ref().repetitionCount <= 0 { @@ -133,4 +132,8 @@ impl Decoder for AvifDecoder<'_> { duration_ts, ))) } + + fn duration_ms(&self) -> i64 { + self.total_duration as i64 * 1000 / self.info.timescale as i64 + } } diff --git a/image-processor/src/worker/process/decoder/libwebp.rs b/image-processor/src/worker/process/decoder/libwebp.rs index bfeecf62..dedae530 100644 --- a/image-processor/src/worker/process/decoder/libwebp.rs +++ b/image-processor/src/worker/process/decoder/libwebp.rs @@ -66,14 +66,13 @@ impl<'data> WebpDecoder<'data> { Ok(Self { info: DecoderInfo { - decoder: DecoderFrontend::LibWebp, - width: info.canvas_width as _, - height: info.canvas_height as _, + width: info.canvas_width as usize, + height: info.canvas_height as usize, loop_count: match info.loop_count { 0 => LoopCount::Infinite, - _ => LoopCount::Finite(info.loop_count as _), + _ => LoopCount::Finite(info.loop_count as usize), }, - frame_count: info.frame_count as _, + frame_count: info.frame_count as usize, timescale: 1000, }, max_input_duration: task @@ -130,4 +129,8 @@ impl Decoder for WebpDecoder<'_> { Ok(Some(FrameRef::new(buf, self.info.width, self.info.height, duration_ts))) } + + fn duration_ms(&self) -> i64 { + self.total_duration as i64 + } } diff --git a/image-processor/src/worker/process/decoder/mod.rs b/image-processor/src/worker/process/decoder/mod.rs index 6769083a..40ff8d07 100644 --- a/image-processor/src/worker/process/decoder/mod.rs +++ b/image-processor/src/worker/process/decoder/mod.rs @@ -96,12 +96,12 @@ pub enum AnyDecoder<'a> { pub trait Decoder { fn backend(&self) -> DecoderFrontend; fn info(&self) -> DecoderInfo; + fn duration_ms(&self) -> i64; fn decode(&mut self) -> Result, DecoderError>; } #[derive(Debug, Clone, Copy)] pub struct DecoderInfo { - pub decoder: DecoderFrontend, pub width: usize, pub height: usize, pub loop_count: LoopCount, @@ -115,6 +115,15 @@ pub enum LoopCount { Finite(usize), } +impl LoopCount { + pub fn as_i32(self) -> i32 { + match self { + LoopCount::Infinite => -1, + LoopCount::Finite(count) => count as i32, + } + } +} + impl Decoder for AnyDecoder<'_> { fn backend(&self) -> DecoderFrontend { match self { @@ -139,4 +148,12 @@ impl Decoder for AnyDecoder<'_> { Self::LibWebp(decoder) => decoder.decode(), } } + + fn duration_ms(&self) -> i64 { + match self { + Self::Ffmpeg(decoder) => decoder.duration_ms(), + Self::LibAvif(decoder) => decoder.duration_ms(), + Self::LibWebp(decoder) => decoder.duration_ms(), + } + } } diff --git a/image-processor/src/worker/process/encoder/gifski.rs b/image-processor/src/worker/process/encoder/gifski.rs index 48de33f1..080d812c 100644 --- a/image-processor/src/worker/process/encoder/gifski.rs +++ b/image-processor/src/worker/process/encoder/gifski.rs @@ -1,6 +1,6 @@ use scuffle_image_processor_proto::OutputQuality; -use super::{Encoder, EncoderBackend, EncoderError, EncoderInfo, EncoderSettings}; +use super::{Encoder, EncoderError, EncoderInfo, EncoderSettings}; use crate::worker::process::decoder::LoopCount; use crate::worker::process::frame::FrameRef; @@ -47,9 +47,7 @@ impl GifskiEncoder { duration: 0, frame_count: 0, format: settings.format, - frontend: EncoderBackend::Gifski, height: 0, - loop_count: settings.loop_count, timescale: settings.timescale, width: 0, }, diff --git a/image-processor/src/worker/process/encoder/libavif.rs b/image-processor/src/worker/process/encoder/libavif.rs index 7086ff34..39f474dd 100644 --- a/image-processor/src/worker/process/encoder/libavif.rs +++ b/image-processor/src/worker/process/encoder/libavif.rs @@ -3,7 +3,7 @@ use std::ptr::NonNull; use libavif_sys::{AVIF_QUALITY_LOSSLESS, AVIF_QUANTIZER_BEST_QUALITY, AVIF_SPEED_FASTEST, AVIF_SPEED_SLOWEST}; use scuffle_image_processor_proto::OutputQuality; -use super::{Encoder, EncoderBackend, EncoderError, EncoderInfo, EncoderSettings}; +use super::{Encoder, EncoderError, EncoderInfo, EncoderSettings}; use crate::worker::process::frame::FrameRef; use crate::worker::process::libavif::AvifError; use crate::worker::process::smart_object::{SmartObject, SmartPtr}; @@ -76,9 +76,7 @@ impl AvifEncoder { duration: 0, frame_count: 0, format: settings.format, - frontend: EncoderBackend::LibAvif, height: 0, - loop_count: settings.loop_count, timescale: settings.timescale, width: 0, }, diff --git a/image-processor/src/worker/process/encoder/libwebp.rs b/image-processor/src/worker/process/encoder/libwebp.rs index d09c68b6..e2d21da5 100644 --- a/image-processor/src/worker/process/encoder/libwebp.rs +++ b/image-processor/src/worker/process/encoder/libwebp.rs @@ -3,7 +3,7 @@ use std::ptr::NonNull; use libwebp_sys::WebPMuxAnimParams; use scuffle_image_processor_proto::OutputQuality; -use super::{Encoder, EncoderBackend, EncoderError, EncoderInfo, EncoderSettings}; +use super::{Encoder, EncoderError, EncoderInfo, EncoderSettings}; use crate::worker::process::decoder::LoopCount; use crate::worker::process::frame::FrameRef; use crate::worker::process::libwebp::{zero_memory_default, WebPError}; @@ -72,9 +72,7 @@ impl WebpEncoder { duration: 0, frame_count: 0, format: settings.format, - frontend: EncoderBackend::LibWebp, height: 0, - loop_count: settings.loop_count, timescale: settings.timescale, width: 0, }, diff --git a/image-processor/src/worker/process/encoder/mod.rs b/image-processor/src/worker/process/encoder/mod.rs index e4fff30e..89bb1343 100644 --- a/image-processor/src/worker/process/encoder/mod.rs +++ b/image-processor/src/worker/process/encoder/mod.rs @@ -32,10 +32,8 @@ pub struct EncoderSettings { pub struct EncoderInfo { pub name: Option, pub format: OutputFormat, - pub frontend: EncoderBackend, pub width: usize, pub height: usize, - pub loop_count: LoopCount, pub timescale: u64, pub duration: u64, pub frame_count: usize, diff --git a/image-processor/src/worker/process/encoder/png.rs b/image-processor/src/worker/process/encoder/png.rs index c4d5b1f6..fb5e8edb 100644 --- a/image-processor/src/worker/process/encoder/png.rs +++ b/image-processor/src/worker/process/encoder/png.rs @@ -1,6 +1,6 @@ use rgb::ComponentBytes; -use super::{Encoder, EncoderBackend, EncoderError, EncoderInfo, EncoderSettings}; +use super::{Encoder, EncoderError, EncoderInfo, EncoderSettings}; use crate::worker::process::frame::FrameRef; pub struct PngEncoder { @@ -18,9 +18,7 @@ impl PngEncoder { duration: 0, frame_count: 0, format: settings.format, - frontend: EncoderBackend::Png, height: 0, - loop_count: settings.loop_count, timescale: settings.timescale, width: 0, }, diff --git a/image-processor/src/worker/process/mod.rs b/image-processor/src/worker/process/mod.rs index cce0e7d6..9d518ff3 100644 --- a/image-processor/src/worker/process/mod.rs +++ b/image-processor/src/worker/process/mod.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use std::sync::Arc; +use blocking::{InputImage, OutputImage}; use bson::oid::ObjectId; use scuffle_foundations::context::Context; -use scuffle_image_processor_proto::{event_callback, ErrorCode, OutputFile, OutputFormat}; +use scuffle_image_processor_proto::{event_callback, DrivePath, ErrorCode, InputFileMetadata, OutputFile, OutputFormat}; use self::blocking::JobOutput; pub use self::decoder::DecoderFrontend; @@ -183,30 +184,17 @@ impl ProcessJob { let job = self.job.clone(); - let ( - decoder_info, - JobOutput { - output: output_results, - input: input_metadata, - }, - ) = blocking::spawn(job.task.clone(), input, self.permit.clone()).await?; + let JobOutput { + output: output_results, + input: input_result, + } = blocking::spawn(job.task.clone(), input.clone(), self.permit.clone()).await?; - let is_animated = output_results.iter().any(|r| r.frame_count > 1); + let has_animated = output_results.iter().any(|r| r.frame_count > 1); let mut files = Vec::new(); for output_result in output_results { - let vars = setup_vars( - self.job.id, - output_result.format_name.clone(), - output_result.format, - output_result.scale, - output_result.width, - output_result.height, - output_result.format_idx, - output_result.resize_idx, - is_animated, - ); + let vars = setup_output_vars(self.job.id, &output_result, has_animated); let file_path = strfmt::strfmt(&output_drive_path.path, &vars).map_err(|err| { tracing::error!("failed to format path: {err}"); @@ -221,14 +209,21 @@ impl ProcessJob { output_result.data.into(), Some(DriveWriteOptions { content_type: Some(content_type(output_result.format).to_owned()), - acl: output.acl_override.clone(), + acl: output_drive_path.acl.clone(), ..Default::default() }), ) .await?; files.push(OutputFile { - path: file_path, + path: Some(DrivePath { + drive: output_drive_path.drive.clone(), + path: file_path, + acl: output_drive_path + .acl + .clone() + .or_else(|| output_drive.default_acl().map(|s| s.to_owned())), + }), size: size as u32, format: output_result.format as i32, frame_count: output_result.frame_count as u32, @@ -236,34 +231,67 @@ impl ProcessJob { width: output_result.width as u32, duration_ms: output_result.duration_ms as u32, content_type: content_type(output_result.format).to_owned(), - acl: output - .acl_override - .as_deref() - .or(output_drive.default_acl()) - .map(|s| s.to_owned()), + loop_count: output_result.loop_count.as_i32(), }); } + let input_path = if let Some(input_reupload_path) = job.task.output.as_ref().unwrap().input_reupload_path.clone() { + let vars = setup_input_vars(self.job.id, &input_result); + + let file_path = strfmt::strfmt(&input_reupload_path.path, &vars).map_err(|err| { + tracing::error!("failed to format path: {err}"); + JobError::Internal("failed to format path") + })?; + + let drive = global.drive(&input_reupload_path.drive).ok_or(JobError::InvalidJob)?; + + drive + .write( + &file_path, + input.clone(), + Some(DriveWriteOptions { + content_type: Some(input_result.format.media_type().to_string()), + acl: input_reupload_path.acl.clone(), + ..Default::default() + }), + ) + .await + .map_err(|err| { + tracing::error!("failed to write input upload: {err}"); + JobError::Internal("failed to write input upload") + })?; + + Some(DrivePath { + drive: input_reupload_path.drive, + path: file_path, + acl: input_reupload_path.acl.or_else(|| drive.default_acl().map(|s| s.to_owned())), + }) + } else { + match self.job.task.input.as_ref().unwrap().path.clone().unwrap() { + scuffle_image_processor_proto::input::Path::DrivePath(drive_path) => Some(drive_path), + _ => None, + } + }; + Ok(event_callback::Success { drive: output_drive_path.drive.clone(), - input_metadata: Some(input_metadata), + input_metadata: Some(InputFileMetadata { + content_type: input_result.format.media_type().to_string(), + width: input_result.width as u32, + height: input_result.height as u32, + frame_count: input_result.frame_count as u32, + duration_ms: input_result.duration_ms as u32, + size: input.len() as u32, + loop_count: input_result.loop_count.as_i32(), + path: input_path, + }), files, }) } } -fn setup_vars( - id: ObjectId, - format_name: Option, - format: OutputFormat, - scale: Option, - width: usize, - height: usize, - format_idx: usize, - resize_idx: usize, - is_animated: bool, -) -> HashMap { - let format_name = format_name.unwrap_or_else(|| match format { +fn setup_output_vars(id: ObjectId, output: &OutputImage, has_animated: bool) -> HashMap { + let format_name = output.format_name.clone().unwrap_or_else(|| match output.format { OutputFormat::AvifAnim => "avif_anim".to_owned(), OutputFormat::AvifStatic => "avif_static".to_owned(), OutputFormat::WebpAnim => "webp_anim".to_owned(), @@ -272,14 +300,14 @@ fn setup_vars( OutputFormat::GifAnim => "gif_anim".to_owned(), }); - let scale = scale.map(|scale| scale.to_string()).unwrap_or_else(|| "".to_owned()); + let scale = output.scale.map(|scale| scale.to_string()).unwrap_or_else(|| "".to_owned()); - let static_ = match format { - OutputFormat::AvifStatic | OutputFormat::PngStatic | OutputFormat::WebpStatic if is_animated => "_static", + let static_ = match output.format { + OutputFormat::AvifStatic | OutputFormat::PngStatic | OutputFormat::WebpStatic if has_animated => "_static", _ => "", }; - let ext = match format { + let ext = match output.format { OutputFormat::AvifAnim | OutputFormat::AvifStatic => "avif", OutputFormat::PngStatic => "png", OutputFormat::WebpAnim | OutputFormat::WebpStatic => "webp", @@ -290,10 +318,10 @@ fn setup_vars( ("id".to_owned(), id.to_string()), ("format".to_owned(), format_name), ("scale".to_owned(), scale), - ("width".to_owned(), width.to_string()), - ("height".to_owned(), height.to_string()), - ("format_idx".to_owned(), format_idx.to_string()), - ("resize_idx".to_owned(), resize_idx.to_string()), + ("width".to_owned(), output.width.to_string()), + ("height".to_owned(), output.height.to_string()), + ("format_idx".to_owned(), output.format_idx.to_string()), + ("resize_idx".to_owned(), output.resize_idx.to_string()), ("static".to_owned(), static_.to_owned()), ("ext".to_owned(), ext.to_owned()), ] @@ -309,3 +337,14 @@ fn content_type(format: OutputFormat) -> &'static str { OutputFormat::GifAnim => "image/gif", } } + +fn setup_input_vars(id: ObjectId, input: &InputImage) -> HashMap { + [ + ("id".to_owned(), id.to_string()), + ("width".to_owned(), input.width.to_string()), + ("height".to_owned(), input.height.to_string()), + ("ext".to_owned(), input.format.extension().to_owned()), + ] + .into_iter() + .collect::>() +}