From b1bbc6b1599b42788d5ecb54f3d4d3e271b8e137 Mon Sep 17 00:00:00 2001 From: asonix Date: Mon, 17 Jul 2023 17:45:26 -0500 Subject: [PATCH] Attempt adding quality settings to pict-rs --- defaults.toml | 3 + pict-rs.toml | 158 +++++++++++++++++++++++++++ src/config.rs | 2 + src/config/commandline.rs | 221 +++++++++++++++++++++++++++++++++++++- src/config/defaults.rs | 26 +++++ src/config/file.rs | 128 ++++++++++++++++++++++ src/validate.rs | 38 ++++--- src/validate/ffmpeg.rs | 19 +++- src/validate/magick.rs | 61 ++++++++++- 9 files changed, 633 insertions(+), 23 deletions(-) diff --git a/defaults.toml b/defaults.toml index acc9704..918bfbd 100644 --- a/defaults.toml +++ b/defaults.toml @@ -54,6 +54,9 @@ max_file_size = 40 max_frame_count = 900 video_codec = "vp9" +[media.video.quality] +crf_max = 32 + [repo] type = "sled" path = "/mnt/sled-repo" diff --git a/pict-rs.toml b/pict-rs.toml index e6940c7..1f53623 100644 --- a/pict-rs.toml +++ b/pict-rs.toml @@ -188,6 +188,53 @@ max_file_size = 40 format = "webp" +[media.image.quality] +## Optional: set quality for AVIF images +# environment variable : PICTRS__MEDIA__IMAGE__QUALITY__AVIF +# default: empty +# +# availabe range: 0-100 +# 100 means best quality and 0 means worst quality. Playing with numbers between 40 and 100 makes +# the most sense. +avif = 100 + +## Optional: set compression for PNG images +# environment variable : PICTRS__MEDIA__IMAGE__QUALITY__PNG +# default: empty +# +# availabe range: 0-100 +# 100 means best compression and 0 means worst compression. Since PNG is a lossless format, changing +# this value will not change how the images look. +png = 100 + +## Optional: set quality for JPEG images +# environment variable : PICTRS__MEDIA__IMAGE__QUALITY__JPEG +# default: empty +# +# availabe range: 0-100 +# 100 means best quality and 0 means worst quality. Playing with numbers between 60 and 100 makes +# the most sense. +jpeg = 100 + +## Optional: set quality for JXL images +# environment variable : PICTRS__MEDIA__IMAGE__QUALITY__JXL +# default: empty +# +# availabe range: 0-100 +# 100 means best quality and 0 means worst quality. Playing with numbers between 40 and 100 makes +# the most sense. +jxl = 100 + +## Optional: set quality for WEBP images +# environment variable : PICTRS__MEDIA__IMAGE__QUALITY__WEBP +# default: empty +# +# availabe range: 0-100 +# 100 means best quality and 0 means worst quality. Playing with numbers between 50 and 100 makes +# the most sense. +webp = 100 + + [media.animation] ## Optional: max animation width (in pixels) # environment variable: PICTRS__MEDIA__ANIMATION__MAX_WIDTH @@ -235,6 +282,35 @@ max_frame_count = 100 format = "webp" +[media.animation.quality] +## Optional: set compression for APNG animations +# environment variable : PICTRS__MEDIA__ANIMATION__QUALITY__APNG +# default: empty +# +# availabe range: 0-100 +# 100 means best compression and 0 means worst compression. Since APNG is a lossless format, +# changing this value will not change how the animations look. +apng = 100 + +## Optional: set quality for AVIF animations +# environment variable : PICTRS__MEDIA__ANIMATION__QUALITY__AVIF +# default: empty +# +# availabe range: 0-100 +# 100 means best quality and 0 means worst quality. Playing with numbers between 40 and 100 makes +# the most sense. +avif = 100 + +## Optional: set quality for WEBP animations +# environment variable : PICTRS__MEDIA__ANIMATION__QUALITY__WEBP +# default: empty +# +# availabe range: 0-100 +# 100 means best quality and 0 means worst quality. Playing with numbers between 50 and 100 makes +# the most sense. +webp = 100 + + [media.video] ## Optional: enable MP4 and WEBM uploads (without sound) # environment variable: PICTRS__MEDIA__VIDEO__ENABLE @@ -308,6 +384,88 @@ video_codec = "vp9" audio_codec = "opus" +[media.video.quality] +## Optional: set maximum quality for all videos +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_MAX +# default: 32 +# +# This value means different things for different video codecs: +# - it ranges from 0 to 63 for AV1 +# - it ranges from 4 to 63 for VP8 +# - it ranges from 0 to 63 for VP9 +# - it ranges from 0 to 51 for H265 +# - it ranges from 0 to 51 for 8bit H264 +# - it ranges from 0 to 63 for 10bit H264 +# +# A lower value (closer to 0) is higher quality, while a higher value (closer to 63) is lower +# quality. Generally acceptable ranges are 15-38, where lower values are preferred for larger +# videos +# +# This value may be overridden for some videos depending on whether other crf configurations are set +# For example, if crf_max is set to 32 and crf_720 is set to 34, then all videos smaller than or +# equal to 720p video will be encoded with a `crf` of 34, while all videos larger than 720p will be +# encoded with a `crf` of 32 +# +# The example values here are taken from a google document about reasonable CRF values for VP9 +# video. More information about `crf` can be found on ffmpeg's wiki +# +# - AV1: https://trac.ffmpeg.org/wiki/Encode/AV1#ConstantQuality +# - H264: https://trac.ffmpeg.org/wiki/Encode/H.264#crf +# - H265: https://trac.ffmpeg.org/wiki/Encode/H.265#ConstantRateFactorCRF +# - VP8: https://trac.ffmpeg.org/wiki/Encode/H.265#ConstantRateFactorCRF +# - VP9: https://trac.ffmpeg.org/wiki/Encode/VP9#constantq +crf_max = 12 + +## Optional: set quality for videos up to 240p +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_240 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 240px (240p) +crf_240 = 37 + +## Optional: set quality for videos up to 360p +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_360 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 360px (260p) +crf_360 = 36 + +## Optional: set quality for videos up to 480p +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_480 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 480px (480p) +crf_480 = 33 + +## Optional: set quality for videos up to 720p +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_720 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 720px (720p) +crf_720 = 32 + +## Optional: set quality for videos up to 1080p +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_1080 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 1080px (1080p) +crf_1080 = 31 + +## Optional: set quality for videos up to 1440p +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_1440 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 1440px (1440p) +crf_1440 = 24 + +## Optional: set quality for videos up to 4K +# environment variable: PICTRS__MEDIA__VIDEO__QUALITY__CRF_2160 +# default: empty +# +# This value overrides `crf_max` for videos with a smaller dimension of at most 2160px (4K) +crf_2160 = 15 + + ## Database configuration [repo] ## Optional: database backend to use diff --git a/src/config.rs b/src/config.rs index f32bc28..71632d0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -111,6 +111,8 @@ pub(crate) fn configure() -> color_eyre::Result<(Configuration, Operation)> { .add_source(config::Config::try_from(&config_format)?) .build()?; + println!("{built:?}"); + let config: Configuration = built.try_deserialize()?; if let Some(save_to) = save_to { diff --git a/src/config/commandline.rs b/src/config/commandline.rs index 6b6b699..a0f45db 100644 --- a/src/config/commandline.rs +++ b/src/config/commandline.rs @@ -55,12 +55,20 @@ impl Args { media_image_max_area, media_image_max_file_size, media_image_format, + media_image_quality_avif, + media_image_quality_jpeg, + media_image_quality_jxl, + media_image_quality_png, + media_image_quality_webp, media_animation_max_width, media_animation_max_height, media_animation_max_area, media_animation_max_file_size, media_animation_max_frame_count, media_animation_format, + media_animation_quality_apng, + media_animation_quality_avif, + media_animation_quality_webp, media_video_enable, media_video_allow_audio, media_video_max_width, @@ -70,6 +78,14 @@ impl Args { media_video_max_frame_count, media_video_codec, media_video_audio_codec, + media_video_quality_max, + media_video_quality_240, + media_video_quality_360, + media_video_quality_480, + media_video_quality_720, + media_video_quality_1080, + media_video_quality_1440, + media_video_quality_2160, media_filters, read_only, store, @@ -86,12 +102,27 @@ impl Args { timeout: client_timeout, }; + let image_quality = ImageQuality { + avif: media_image_quality_avif, + jpeg: media_image_quality_jpeg, + jxl: media_image_quality_jxl, + png: media_image_quality_png, + webp: media_image_quality_webp, + }; + let image = Image { max_file_size: media_image_max_file_size, max_width: media_image_max_width, max_height: media_image_max_height, max_area: media_image_max_area, format: media_image_format, + quality: image_quality.set(), + }; + + let animation_quality = AnimationQuality { + apng: media_animation_quality_apng, + avif: media_animation_quality_avif, + webp: media_animation_quality_webp, }; let animation = Animation { @@ -101,6 +132,18 @@ impl Args { max_area: media_animation_max_area, max_frame_count: media_animation_max_frame_count, format: media_animation_format, + quality: animation_quality.set(), + }; + + let video_quality = VideoQuality { + crf_240: media_video_quality_240, + crf_360: media_video_quality_360, + crf_480: media_video_quality_480, + crf_720: media_video_quality_720, + crf_1080: media_video_quality_1080, + crf_1440: media_video_quality_1440, + crf_2160: media_video_quality_2160, + crf_max: media_video_quality_max, }; let video = Video { @@ -113,6 +156,7 @@ impl Args { max_frame_count: media_video_max_frame_count, video_codec: media_video_codec, audio_codec: media_video_audio_codec, + quality: video_quality.set(), }; let media = Media { @@ -404,6 +448,8 @@ struct Image { max_file_size: Option, #[serde(skip_serializing_if = "Option::is_none")] format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + quality: Option, } impl Image { @@ -412,7 +458,38 @@ impl Image { || self.max_height.is_some() || self.max_area.is_some() || self.max_file_size.is_some() - || self.format.is_some(); + || self.format.is_some() + || self.quality.is_some(); + + if any_set { + Some(self) + } else { + None + } + } +} + +#[derive(Debug, Default, serde::Serialize)] +struct ImageQuality { + #[serde(skip_serializing_if = "Option::is_none")] + avif: Option, + #[serde(skip_serializing_if = "Option::is_none")] + jpeg: Option, + #[serde(skip_serializing_if = "Option::is_none")] + jxl: Option, + #[serde(skip_serializing_if = "Option::is_none")] + png: Option, + #[serde(skip_serializing_if = "Option::is_none")] + webp: Option, +} + +impl ImageQuality { + fn set(self) -> Option { + let any_set = self.avif.is_some() + || self.jpeg.is_some() + || self.jxl.is_some() + || self.png.is_some() + || self.webp.is_some(); if any_set { Some(self) @@ -437,6 +514,8 @@ struct Animation { max_file_size: Option, #[serde(skip_serializing_if = "Option::is_none")] format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + quality: Option, } impl Animation { @@ -446,7 +525,31 @@ impl Animation { || self.max_area.is_some() || self.max_frame_count.is_some() || self.max_file_size.is_some() - || self.format.is_some(); + || self.format.is_some() + || self.quality.is_some(); + + if any_set { + Some(self) + } else { + None + } + } +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct AnimationQuality { + #[serde(skip_serializing_if = "Option::is_none")] + apng: Option, + #[serde(skip_serializing_if = "Option::is_none")] + avif: Option, + #[serde(skip_serializing_if = "Option::is_none")] + webp: Option, +} + +impl AnimationQuality { + fn set(self) -> Option { + let any_set = self.apng.is_some() || self.avif.is_some() || self.webp.is_some(); if any_set { Some(self) @@ -477,6 +580,8 @@ struct Video { video_codec: Option, #[serde(skip_serializing_if = "Option::is_none")] audio_codec: Option, + #[serde(skip_serializing_if = "Option::is_none")] + quality: Option, } impl Video { @@ -489,7 +594,41 @@ impl Video { || self.max_frame_count.is_some() || self.max_file_size.is_some() || self.video_codec.is_some() - || self.audio_codec.is_some(); + || self.audio_codec.is_some() + || self.quality.is_some(); + + if any_set { + Some(self) + } else { + None + } + } +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VideoQuality { + crf_240: Option, + crf_360: Option, + crf_480: Option, + crf_720: Option, + crf_1080: Option, + crf_1440: Option, + crf_2160: Option, + crf_max: Option, +} + +impl VideoQuality { + fn set(self) -> Option { + let any_set = self.crf_240.is_some() + || self.crf_360.is_some() + || self.crf_480.is_some() + || self.crf_720.is_some() + || self.crf_1080.is_some() + || self.crf_1440.is_some() + || self.crf_1440.is_some() + || self.crf_2160.is_some() + || self.crf_max.is_some(); if any_set { Some(self) @@ -609,6 +748,31 @@ struct Run { /// Enforce a specific format for uploaded images #[arg(long)] media_image_format: Option, + /// Enforce a specific quality for AVIF images + /// + /// A higher number means better quality, with a minimum value of 0 and a maximum value of 100 + #[arg(long)] + media_image_quality_avif: Option, + /// Enforce a specific compression level for PNG images + /// + /// A higher number means better compression. PNGs will look the same regardless + #[arg(long)] + media_image_quality_png: Option, + /// Enforce a specific quality for JPEG images + /// + /// A higher number means better quality, with a minimum value of 0 and a maximum value of 100 + #[arg(long)] + media_image_quality_jpeg: Option, + /// Enforce a specific quality for JXL images + /// + /// A higher number means better quality, with a minimum value of 0 and a maximum value of 100 + #[arg(long)] + media_image_quality_jxl: Option, + /// Enforce a specific quality for WEBP images + /// + /// A higher number means better quality, with a minimum value of 0 and a maximum value of 100 + #[arg(long)] + media_image_quality_webp: Option, /// The maximum width, in pixels, for uploaded animations #[arg(long)] @@ -628,6 +792,21 @@ struct Run { /// Enforce a specific format for uploaded animations #[arg(long)] media_animation_format: Option, + /// Enforce a specific compression level for APNG animations + /// + /// A higher number means better compression, APNGs will look the same regardless + #[arg(long)] + media_animation_quality_apng: Option, + /// Enforce a specific quality for AVIF animations + /// + /// A higher number means better quality, with a minimum value of 0 and a maximum value of 100 + #[arg(long)] + media_animation_quality_avif: Option, + /// Enforce a specific quality for WEBP animations + /// + /// A higher number means better quality, with a minimum value of 0 and a maximum value of 100 + #[arg(long)] + media_animation_quality_webp: Option, /// Whether to enable video uploads #[arg(long)] @@ -656,6 +835,42 @@ struct Run { /// Enforce a specific audio codec for uploaded videos #[arg(long)] media_video_audio_codec: Option, + /// Enforce a maximum quality level for uploaded videos + /// + /// This value means different things for different video codecs: + /// - it ranges from 0 to 63 for AV1 + /// - it ranges from 4 to 63 for VP8 + /// - it ranges from 0 to 63 for VP9 + /// - it ranges from 0 to 51 for H265 + /// - it ranges from 0 to 51 for 8bit H264 + /// - it ranges from 0 to 63 for 10bit H264 + /// + /// A lower value (closer to 0) is higher quality, while a higher value (closer to 63) is lower + /// quality. Generally acceptable ranges are 15-38, where lower values are preferred for larger + /// videos + #[arg(long)] + media_video_quality_max: Option, + /// Enforce a video quality for video with a smaller dimension less than 240px + #[arg(long)] + media_video_quality_240: Option, + /// Enforce a video quality for video with a smaller dimension less than 360px + #[arg(long)] + media_video_quality_360: Option, + /// Enforce a video quality for video with a smaller dimension less than 480px + #[arg(long)] + media_video_quality_480: Option, + /// Enforce a video quality for video with a smaller dimension less than 720px + #[arg(long)] + media_video_quality_720: Option, + /// Enforce a video quality for video with a smaller dimension less than 1080px + #[arg(long)] + media_video_quality_1080: Option, + /// Enforce a video quality for video with a smaller dimension less than 1440px + #[arg(long)] + media_video_quality_1440: Option, + /// Enforce a video quality for video with a smaller dimension less than 2160px + #[arg(long)] + media_video_quality_2160: Option, /// Don't permit ingesting media #[arg(long)] diff --git a/src/config/defaults.rs b/src/config/defaults.rs index 6121648..b5ae42b 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -85,8 +85,13 @@ struct ImageDefaults { max_height: u16, max_area: u32, max_file_size: usize, + quality: ImageQualityDefaults, } +#[derive(Clone, Debug, Default, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct ImageQualityDefaults {} + #[derive(Clone, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] struct AnimationDefaults { @@ -95,8 +100,13 @@ struct AnimationDefaults { max_area: u32, max_frame_count: u32, max_file_size: usize, + quality: AnimationQualityDefaults, } +#[derive(Clone, Debug, Default, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct AnimationQualityDefaults {} + #[derive(Clone, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] struct VideoDefaults { @@ -108,6 +118,13 @@ struct VideoDefaults { max_frame_count: u32, max_file_size: usize, video_codec: VideoCodec, + quality: VideoQualityDefaults, +} + +#[derive(Clone, Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VideoQualityDefaults { + crf_max: u8, } #[derive(Clone, Debug, serde::Serialize)] @@ -232,6 +249,7 @@ impl Default for ImageDefaults { max_height: 10_000, max_area: 40_000_000, max_file_size: 40, + quality: ImageQualityDefaults {}, } } } @@ -244,6 +262,7 @@ impl Default for AnimationDefaults { max_area: 65_536, max_frame_count: 100, max_file_size: 40, + quality: AnimationQualityDefaults {}, } } } @@ -259,10 +278,17 @@ impl Default for VideoDefaults { max_frame_count: 900, max_file_size: 40, video_codec: VideoCodec::Vp9, + quality: VideoQualityDefaults::default(), } } } +impl Default for VideoQualityDefaults { + fn default() -> Self { + VideoQualityDefaults { crf_max: 32 } + } +} + impl Default for RepoDefaults { fn default() -> Self { Self::Sled(SledDefaults::default()) diff --git a/src/config/file.rs b/src/config/file.rs index 48a952c..abd9bdb 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -181,6 +181,42 @@ pub(crate) struct Image { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) format: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) quality: Option, +} + +impl Image { + pub(crate) fn quality_for(&self, format: ImageFormat) -> Option { + self.quality + .as_ref() + .and_then(|quality| match format { + ImageFormat::Avif => quality.avif, + ImageFormat::Jpeg => quality.jpeg, + ImageFormat::Jxl => quality.jxl, + ImageFormat::Png => quality.png, + ImageFormat::Webp => quality.webp, + }) + .map(|quality| quality.min(100)) + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct ImageQuality { + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) avif: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) png: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) jpeg: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) jxl: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) webp: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] @@ -197,6 +233,35 @@ pub(crate) struct Animation { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) format: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) quality: Option, +} + +impl Animation { + pub(crate) fn quality_for(&self, format: AnimationFormat) -> Option { + self.quality + .as_ref() + .and_then(|quality| match format { + AnimationFormat::Apng => quality.apng, + AnimationFormat::Avif => quality.avif, + AnimationFormat::Gif => None, + AnimationFormat::Webp => quality.webp, + }) + .map(|quality| quality.min(100)) + } +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub(crate) struct AnimationQuality { + #[serde(skip_serializing_if = "Option::is_none")] + apng: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + avif: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + webp: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] @@ -217,10 +282,73 @@ pub(crate) struct Video { pub(crate) video_codec: VideoCodec, + pub(crate) quality: VideoQuality, + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) audio_codec: Option, } +impl Video { + pub(crate) fn crf_for(&self, width: u16, height: u16) -> u8 { + let smaller_dimension = width.min(height); + + let dimension_cutoffs = [240, 360, 480, 720, 1080, 1440, 2160]; + let crfs = [ + self.quality.crf_240, + self.quality.crf_360, + self.quality.crf_480, + self.quality.crf_720, + self.quality.crf_1080, + self.quality.crf_1440, + self.quality.crf_2160, + ]; + + let index = dimension_cutoffs + .into_iter() + .enumerate() + .find_map(|(index, dim)| { + if smaller_dimension <= dim { + Some(index) + } else { + None + } + }) + .unwrap_or(crfs.len()); + + crfs.into_iter() + .skip(index) + .find_map(|opt| opt) + .unwrap_or(self.quality.crf_max) + .min(63) + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct VideoQuality { + #[serde(skip_serializing_if = "Option::is_none")] + crf_240: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + crf_360: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + crf_480: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + crf_720: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + crf_1080: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + crf_1440: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + crf_2160: Option, + + crf_max: u8, +} + impl Media { pub(crate) fn preprocess_steps(&self) -> Option<&[(String, String)]> { static PREPROCESS_STEPS: OnceCell> = OnceCell::new(); diff --git a/src/validate.rs b/src/validate.rs index 9a95d28..00e29bb 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -119,7 +119,9 @@ async fn process_image( } = input.build_output(validations.format); let read = if needs_transcode { - Either::left(magick::convert_image(input.format, format, bytes).await?) + let quality = validations.quality_for(format); + + Either::left(magick::convert_image(input.format, format, quality, bytes).await?) } else { Either::right(exiftool::clear_metadata_bytes_read(bytes)?) }; @@ -170,27 +172,31 @@ async fn process_animation( } = input.build_output(validations.animation.format); let read = if needs_transcode { - Either::left(magick::convert_animation(input, format, bytes).await?) + let quality = validations.animation.quality_for(format); + + Either::left(magick::convert_animation(input, format, quality, bytes).await?) } else { Either::right(Either::left(exiftool::clear_metadata_bytes_read(bytes)?)) }; Ok((InternalFormat::Animation(format), read)) } - Err(_) if validate_video(bytes.len(), width, height, frames, validations.video).is_ok() => { - let output = OutputVideoFormat::from_parts( - validations.video.video_codec, - validations.video.audio_codec, - validations.video.allow_audio, - ); + Err(_) => match validate_video(bytes.len(), width, height, frames, validations.video) { + Ok(()) => { + let output = OutputVideoFormat::from_parts( + validations.video.video_codec, + validations.video.audio_codec, + validations.video.allow_audio, + ); - let read = Either::right(Either::right( - magick::convert_video(input, output, bytes).await?, - )); + let read = Either::right(Either::right( + magick::convert_video(input, output, bytes).await?, + )); - Ok((InternalFormat::Video(output.internal_format()), read)) - } - Err(e) => Err(e.into()), + Ok((InternalFormat::Video(output.internal_format()), read)) + } + Err(e) => Err(e.into()), + }, } } @@ -240,7 +246,9 @@ async fn process_video( validations.allow_audio, ); - let read = ffmpeg::transcode_bytes(input, output, bytes).await?; + let crf = validations.crf_for(width, height); + + let read = ffmpeg::transcode_bytes(input, output, crf, bytes).await?; Ok((InternalFormat::Video(output.internal_format()), read)) } diff --git a/src/validate/ffmpeg.rs b/src/validate/ffmpeg.rs index 829cc85..8ec390f 100644 --- a/src/validate/ffmpeg.rs +++ b/src/validate/ffmpeg.rs @@ -10,6 +10,7 @@ use crate::{ pub(super) async fn transcode_bytes( input_format: VideoFormat, output_format: OutputVideoFormat, + crf: u8, bytes: Bytes, ) -> Result { let input_file = crate::tmp_file::tmp_file(None); @@ -30,7 +31,14 @@ pub(super) async fn transcode_bytes( let output_file = crate::tmp_file::tmp_file(None); let output_file_str = output_file.to_str().ok_or(FfMpegError::Path)?; - transcode_files(input_file_str, input_format, output_file_str, output_format).await?; + transcode_files( + input_file_str, + input_format, + output_file_str, + output_format, + crf, + ) + .await?; let tmp_two = crate::file::File::open(&output_file) .await @@ -50,6 +58,7 @@ async fn transcode_files( input_format: VideoFormat, output_path: &str, output_format: OutputVideoFormat, + crf: u8, ) -> Result<(), FfMpegError> { if let Some(audio_codec) = output_format.ffmpeg_audio_codec() { Process::run( @@ -70,6 +79,10 @@ async fn transcode_files( audio_codec, "-c:v", output_format.ffmpeg_video_codec(), + "-b:v", + "0", + "-crf", + &crf.to_string(), "-f", output_format.ffmpeg_format(), output_path, @@ -95,6 +108,10 @@ async fn transcode_files( "-an", "-c:v", output_format.ffmpeg_video_codec(), + "-b:v", + "0", + "-crf", + &crf.to_string(), "-f", output_format.ffmpeg_format(), output_path, diff --git a/src/validate/magick.rs b/src/validate/magick.rs index 64629e4..5b36bbc 100644 --- a/src/validate/magick.rs +++ b/src/validate/magick.rs @@ -10,17 +10,33 @@ use crate::{ pub(super) async fn convert_image( input: ImageFormat, output: ImageFormat, + quality: Option, bytes: Bytes, ) -> Result { - convert(input.magick_format(), output.magick_format(), false, bytes).await + convert( + input.magick_format(), + output.magick_format(), + false, + quality, + bytes, + ) + .await } pub(super) async fn convert_animation( input: AnimationFormat, output: AnimationFormat, + quality: Option, bytes: Bytes, ) -> Result { - convert(input.magick_format(), output.magick_format(), true, bytes).await + convert( + input.magick_format(), + output.magick_format(), + true, + quality, + bytes, + ) + .await } pub(super) async fn convert_video( @@ -28,13 +44,21 @@ pub(super) async fn convert_video( output: OutputVideoFormat, bytes: Bytes, ) -> Result { - convert(input.magick_format(), output.magick_format(), true, bytes).await + convert( + input.magick_format(), + output.magick_format(), + true, + None, + bytes, + ) + .await } async fn convert( input: &'static str, output: &'static str, coalesce: bool, + quality: Option, bytes: Bytes, ) -> Result { let input_file = crate::tmp_file::tmp_file(None); @@ -57,6 +81,34 @@ async fn convert( let output_arg = format!("{output}:-"); let process = if coalesce { + if let Some(quality) = quality { + Process::run( + "magick", + &[ + "convert", + "-strip", + "-auto-orient", + &input_arg, + "-quality", + &quality.to_string(), + "-coalesce", + &output_arg, + ], + )? + } else { + Process::run( + "magick", + &[ + "convert", + "-strip", + "-auto-orient", + &input_arg, + "-coalesce", + &output_arg, + ], + )? + } + } else if let Some(quality) = quality { Process::run( "magick", &[ @@ -64,7 +116,8 @@ async fn convert( "-strip", "-auto-orient", &input_arg, - "-coalesce", + "-quality", + &quality.to_string(), &output_arg, ], )?