2
0
Fork 0
mirror of https://git.asonix.dog/asonix/pict-rs synced 2024-12-22 19:31:35 +00:00

Attempt adding quality settings to pict-rs

This commit is contained in:
asonix 2023-07-17 17:45:26 -05:00
parent a70dbe5c57
commit b1bbc6b159
9 changed files with 633 additions and 23 deletions

View file

@ -54,6 +54,9 @@ max_file_size = 40
max_frame_count = 900 max_frame_count = 900
video_codec = "vp9" video_codec = "vp9"
[media.video.quality]
crf_max = 32
[repo] [repo]
type = "sled" type = "sled"
path = "/mnt/sled-repo" path = "/mnt/sled-repo"

View file

@ -188,6 +188,53 @@ max_file_size = 40
format = "webp" 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] [media.animation]
## Optional: max animation width (in pixels) ## Optional: max animation width (in pixels)
# environment variable: PICTRS__MEDIA__ANIMATION__MAX_WIDTH # environment variable: PICTRS__MEDIA__ANIMATION__MAX_WIDTH
@ -235,6 +282,35 @@ max_frame_count = 100
format = "webp" 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] [media.video]
## Optional: enable MP4 and WEBM uploads (without sound) ## Optional: enable MP4 and WEBM uploads (without sound)
# environment variable: PICTRS__MEDIA__VIDEO__ENABLE # environment variable: PICTRS__MEDIA__VIDEO__ENABLE
@ -308,6 +384,88 @@ video_codec = "vp9"
audio_codec = "opus" 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 ## Database configuration
[repo] [repo]
## Optional: database backend to use ## Optional: database backend to use

View file

@ -111,6 +111,8 @@ pub(crate) fn configure() -> color_eyre::Result<(Configuration, Operation)> {
.add_source(config::Config::try_from(&config_format)?) .add_source(config::Config::try_from(&config_format)?)
.build()?; .build()?;
println!("{built:?}");
let config: Configuration = built.try_deserialize()?; let config: Configuration = built.try_deserialize()?;
if let Some(save_to) = save_to { if let Some(save_to) = save_to {

View file

@ -55,12 +55,20 @@ impl Args {
media_image_max_area, media_image_max_area,
media_image_max_file_size, media_image_max_file_size,
media_image_format, 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_width,
media_animation_max_height, media_animation_max_height,
media_animation_max_area, media_animation_max_area,
media_animation_max_file_size, media_animation_max_file_size,
media_animation_max_frame_count, media_animation_max_frame_count,
media_animation_format, media_animation_format,
media_animation_quality_apng,
media_animation_quality_avif,
media_animation_quality_webp,
media_video_enable, media_video_enable,
media_video_allow_audio, media_video_allow_audio,
media_video_max_width, media_video_max_width,
@ -70,6 +78,14 @@ impl Args {
media_video_max_frame_count, media_video_max_frame_count,
media_video_codec, media_video_codec,
media_video_audio_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, media_filters,
read_only, read_only,
store, store,
@ -86,12 +102,27 @@ impl Args {
timeout: client_timeout, 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 { let image = Image {
max_file_size: media_image_max_file_size, max_file_size: media_image_max_file_size,
max_width: media_image_max_width, max_width: media_image_max_width,
max_height: media_image_max_height, max_height: media_image_max_height,
max_area: media_image_max_area, max_area: media_image_max_area,
format: media_image_format, 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 { let animation = Animation {
@ -101,6 +132,18 @@ impl Args {
max_area: media_animation_max_area, max_area: media_animation_max_area,
max_frame_count: media_animation_max_frame_count, max_frame_count: media_animation_max_frame_count,
format: media_animation_format, 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 { let video = Video {
@ -113,6 +156,7 @@ impl Args {
max_frame_count: media_video_max_frame_count, max_frame_count: media_video_max_frame_count,
video_codec: media_video_codec, video_codec: media_video_codec,
audio_codec: media_video_audio_codec, audio_codec: media_video_audio_codec,
quality: video_quality.set(),
}; };
let media = Media { let media = Media {
@ -404,6 +448,8 @@ struct Image {
max_file_size: Option<usize>, max_file_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
format: Option<ImageFormat>, format: Option<ImageFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
quality: Option<ImageQuality>,
} }
impl Image { impl Image {
@ -412,7 +458,38 @@ impl Image {
|| self.max_height.is_some() || self.max_height.is_some()
|| self.max_area.is_some() || self.max_area.is_some()
|| self.max_file_size.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<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
jpeg: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
jxl: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
png: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
webp: Option<u8>,
}
impl ImageQuality {
fn set(self) -> Option<Self> {
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 { if any_set {
Some(self) Some(self)
@ -437,6 +514,8 @@ struct Animation {
max_file_size: Option<usize>, max_file_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
format: Option<AnimationFormat>, format: Option<AnimationFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
quality: Option<AnimationQuality>,
} }
impl Animation { impl Animation {
@ -446,7 +525,31 @@ impl Animation {
|| self.max_area.is_some() || self.max_area.is_some()
|| self.max_frame_count.is_some() || self.max_frame_count.is_some()
|| self.max_file_size.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<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
avif: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
webp: Option<u8>,
}
impl AnimationQuality {
fn set(self) -> Option<Self> {
let any_set = self.apng.is_some() || self.avif.is_some() || self.webp.is_some();
if any_set { if any_set {
Some(self) Some(self)
@ -477,6 +580,8 @@ struct Video {
video_codec: Option<VideoCodec>, video_codec: Option<VideoCodec>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
audio_codec: Option<AudioCodec>, audio_codec: Option<AudioCodec>,
#[serde(skip_serializing_if = "Option::is_none")]
quality: Option<VideoQuality>,
} }
impl Video { impl Video {
@ -489,7 +594,41 @@ impl Video {
|| self.max_frame_count.is_some() || self.max_frame_count.is_some()
|| self.max_file_size.is_some() || self.max_file_size.is_some()
|| self.video_codec.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<u8>,
crf_360: Option<u8>,
crf_480: Option<u8>,
crf_720: Option<u8>,
crf_1080: Option<u8>,
crf_1440: Option<u8>,
crf_2160: Option<u8>,
crf_max: Option<u8>,
}
impl VideoQuality {
fn set(self) -> Option<Self> {
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 { if any_set {
Some(self) Some(self)
@ -609,6 +748,31 @@ struct Run {
/// Enforce a specific format for uploaded images /// Enforce a specific format for uploaded images
#[arg(long)] #[arg(long)]
media_image_format: Option<ImageFormat>, media_image_format: Option<ImageFormat>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// The maximum width, in pixels, for uploaded animations /// The maximum width, in pixels, for uploaded animations
#[arg(long)] #[arg(long)]
@ -628,6 +792,21 @@ struct Run {
/// Enforce a specific format for uploaded animations /// Enforce a specific format for uploaded animations
#[arg(long)] #[arg(long)]
media_animation_format: Option<AnimationFormat>, media_animation_format: Option<AnimationFormat>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// Whether to enable video uploads /// Whether to enable video uploads
#[arg(long)] #[arg(long)]
@ -656,6 +835,42 @@ struct Run {
/// Enforce a specific audio codec for uploaded videos /// Enforce a specific audio codec for uploaded videos
#[arg(long)] #[arg(long)]
media_video_audio_codec: Option<AudioCodec>, media_video_audio_codec: Option<AudioCodec>,
/// 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<u8>,
/// Enforce a video quality for video with a smaller dimension less than 240px
#[arg(long)]
media_video_quality_240: Option<u8>,
/// Enforce a video quality for video with a smaller dimension less than 360px
#[arg(long)]
media_video_quality_360: Option<u8>,
/// Enforce a video quality for video with a smaller dimension less than 480px
#[arg(long)]
media_video_quality_480: Option<u8>,
/// Enforce a video quality for video with a smaller dimension less than 720px
#[arg(long)]
media_video_quality_720: Option<u8>,
/// Enforce a video quality for video with a smaller dimension less than 1080px
#[arg(long)]
media_video_quality_1080: Option<u8>,
/// Enforce a video quality for video with a smaller dimension less than 1440px
#[arg(long)]
media_video_quality_1440: Option<u8>,
/// Enforce a video quality for video with a smaller dimension less than 2160px
#[arg(long)]
media_video_quality_2160: Option<u8>,
/// Don't permit ingesting media /// Don't permit ingesting media
#[arg(long)] #[arg(long)]

View file

@ -85,8 +85,13 @@ struct ImageDefaults {
max_height: u16, max_height: u16,
max_area: u32, max_area: u32,
max_file_size: usize, max_file_size: usize,
quality: ImageQualityDefaults,
} }
#[derive(Clone, Debug, Default, serde::Serialize)]
#[serde(rename_all = "snake_case")]
struct ImageQualityDefaults {}
#[derive(Clone, Debug, serde::Serialize)] #[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
struct AnimationDefaults { struct AnimationDefaults {
@ -95,8 +100,13 @@ struct AnimationDefaults {
max_area: u32, max_area: u32,
max_frame_count: u32, max_frame_count: u32,
max_file_size: usize, max_file_size: usize,
quality: AnimationQualityDefaults,
} }
#[derive(Clone, Debug, Default, serde::Serialize)]
#[serde(rename_all = "snake_case")]
struct AnimationQualityDefaults {}
#[derive(Clone, Debug, serde::Serialize)] #[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
struct VideoDefaults { struct VideoDefaults {
@ -108,6 +118,13 @@ struct VideoDefaults {
max_frame_count: u32, max_frame_count: u32,
max_file_size: usize, max_file_size: usize,
video_codec: VideoCodec, 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)] #[derive(Clone, Debug, serde::Serialize)]
@ -232,6 +249,7 @@ impl Default for ImageDefaults {
max_height: 10_000, max_height: 10_000,
max_area: 40_000_000, max_area: 40_000_000,
max_file_size: 40, max_file_size: 40,
quality: ImageQualityDefaults {},
} }
} }
} }
@ -244,6 +262,7 @@ impl Default for AnimationDefaults {
max_area: 65_536, max_area: 65_536,
max_frame_count: 100, max_frame_count: 100,
max_file_size: 40, max_file_size: 40,
quality: AnimationQualityDefaults {},
} }
} }
} }
@ -259,10 +278,17 @@ impl Default for VideoDefaults {
max_frame_count: 900, max_frame_count: 900,
max_file_size: 40, max_file_size: 40,
video_codec: VideoCodec::Vp9, video_codec: VideoCodec::Vp9,
quality: VideoQualityDefaults::default(),
} }
} }
} }
impl Default for VideoQualityDefaults {
fn default() -> Self {
VideoQualityDefaults { crf_max: 32 }
}
}
impl Default for RepoDefaults { impl Default for RepoDefaults {
fn default() -> Self { fn default() -> Self {
Self::Sled(SledDefaults::default()) Self::Sled(SledDefaults::default())

View file

@ -181,6 +181,42 @@ pub(crate) struct Image {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub(crate) format: Option<ImageFormat>, pub(crate) format: Option<ImageFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) quality: Option<ImageQuality>,
}
impl Image {
pub(crate) fn quality_for(&self, format: ImageFormat) -> Option<u8> {
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<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) png: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) jpeg: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) jxl: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) webp: Option<u8>,
} }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
@ -197,6 +233,35 @@ pub(crate) struct Animation {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub(crate) format: Option<AnimationFormat>, pub(crate) format: Option<AnimationFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) quality: Option<AnimationQuality>,
}
impl Animation {
pub(crate) fn quality_for(&self, format: AnimationFormat) -> Option<u8> {
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<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
avif: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
webp: Option<u8>,
} }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
@ -217,10 +282,73 @@ pub(crate) struct Video {
pub(crate) video_codec: VideoCodec, pub(crate) video_codec: VideoCodec,
pub(crate) quality: VideoQuality,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub(crate) audio_codec: Option<AudioCodec>, pub(crate) audio_codec: Option<AudioCodec>,
} }
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<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
crf_360: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
crf_480: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
crf_720: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
crf_1080: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
crf_1440: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
crf_2160: Option<u8>,
crf_max: u8,
}
impl Media { impl Media {
pub(crate) fn preprocess_steps(&self) -> Option<&[(String, String)]> { pub(crate) fn preprocess_steps(&self) -> Option<&[(String, String)]> {
static PREPROCESS_STEPS: OnceCell<Vec<(String, String)>> = OnceCell::new(); static PREPROCESS_STEPS: OnceCell<Vec<(String, String)>> = OnceCell::new();

View file

@ -119,7 +119,9 @@ async fn process_image(
} = input.build_output(validations.format); } = input.build_output(validations.format);
let read = if needs_transcode { 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 { } else {
Either::right(exiftool::clear_metadata_bytes_read(bytes)?) Either::right(exiftool::clear_metadata_bytes_read(bytes)?)
}; };
@ -170,14 +172,17 @@ async fn process_animation(
} = input.build_output(validations.animation.format); } = input.build_output(validations.animation.format);
let read = if needs_transcode { 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 { } else {
Either::right(Either::left(exiftool::clear_metadata_bytes_read(bytes)?)) Either::right(Either::left(exiftool::clear_metadata_bytes_read(bytes)?))
}; };
Ok((InternalFormat::Animation(format), read)) Ok((InternalFormat::Animation(format), read))
} }
Err(_) if validate_video(bytes.len(), width, height, frames, validations.video).is_ok() => { Err(_) => match validate_video(bytes.len(), width, height, frames, validations.video) {
Ok(()) => {
let output = OutputVideoFormat::from_parts( let output = OutputVideoFormat::from_parts(
validations.video.video_codec, validations.video.video_codec,
validations.video.audio_codec, validations.video.audio_codec,
@ -191,6 +196,7 @@ async fn process_animation(
Ok((InternalFormat::Video(output.internal_format()), read)) Ok((InternalFormat::Video(output.internal_format()), read))
} }
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
},
} }
} }
@ -240,7 +246,9 @@ async fn process_video(
validations.allow_audio, 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)) Ok((InternalFormat::Video(output.internal_format()), read))
} }

View file

@ -10,6 +10,7 @@ use crate::{
pub(super) async fn transcode_bytes( pub(super) async fn transcode_bytes(
input_format: VideoFormat, input_format: VideoFormat,
output_format: OutputVideoFormat, output_format: OutputVideoFormat,
crf: u8,
bytes: Bytes, bytes: Bytes,
) -> Result<impl AsyncRead + Unpin, FfMpegError> { ) -> Result<impl AsyncRead + Unpin, FfMpegError> {
let input_file = crate::tmp_file::tmp_file(None); 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 = crate::tmp_file::tmp_file(None);
let output_file_str = output_file.to_str().ok_or(FfMpegError::Path)?; 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) let tmp_two = crate::file::File::open(&output_file)
.await .await
@ -50,6 +58,7 @@ async fn transcode_files(
input_format: VideoFormat, input_format: VideoFormat,
output_path: &str, output_path: &str,
output_format: OutputVideoFormat, output_format: OutputVideoFormat,
crf: u8,
) -> Result<(), FfMpegError> { ) -> Result<(), FfMpegError> {
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() { if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
Process::run( Process::run(
@ -70,6 +79,10 @@ async fn transcode_files(
audio_codec, audio_codec,
"-c:v", "-c:v",
output_format.ffmpeg_video_codec(), output_format.ffmpeg_video_codec(),
"-b:v",
"0",
"-crf",
&crf.to_string(),
"-f", "-f",
output_format.ffmpeg_format(), output_format.ffmpeg_format(),
output_path, output_path,
@ -95,6 +108,10 @@ async fn transcode_files(
"-an", "-an",
"-c:v", "-c:v",
output_format.ffmpeg_video_codec(), output_format.ffmpeg_video_codec(),
"-b:v",
"0",
"-crf",
&crf.to_string(),
"-f", "-f",
output_format.ffmpeg_format(), output_format.ffmpeg_format(),
output_path, output_path,

View file

@ -10,17 +10,33 @@ use crate::{
pub(super) async fn convert_image( pub(super) async fn convert_image(
input: ImageFormat, input: ImageFormat,
output: ImageFormat, output: ImageFormat,
quality: Option<u8>,
bytes: Bytes, bytes: Bytes,
) -> Result<impl AsyncRead + Unpin, MagickError> { ) -> Result<impl AsyncRead + Unpin, MagickError> {
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( pub(super) async fn convert_animation(
input: AnimationFormat, input: AnimationFormat,
output: AnimationFormat, output: AnimationFormat,
quality: Option<u8>,
bytes: Bytes, bytes: Bytes,
) -> Result<impl AsyncRead + Unpin, MagickError> { ) -> Result<impl AsyncRead + Unpin, MagickError> {
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( pub(super) async fn convert_video(
@ -28,13 +44,21 @@ pub(super) async fn convert_video(
output: OutputVideoFormat, output: OutputVideoFormat,
bytes: Bytes, bytes: Bytes,
) -> Result<impl AsyncRead + Unpin, MagickError> { ) -> Result<impl AsyncRead + Unpin, MagickError> {
convert(input.magick_format(), output.magick_format(), true, bytes).await convert(
input.magick_format(),
output.magick_format(),
true,
None,
bytes,
)
.await
} }
async fn convert( async fn convert(
input: &'static str, input: &'static str,
output: &'static str, output: &'static str,
coalesce: bool, coalesce: bool,
quality: Option<u8>,
bytes: Bytes, bytes: Bytes,
) -> Result<impl AsyncRead + Unpin, MagickError> { ) -> Result<impl AsyncRead + Unpin, MagickError> {
let input_file = crate::tmp_file::tmp_file(None); let input_file = crate::tmp_file::tmp_file(None);
@ -57,6 +81,21 @@ async fn convert(
let output_arg = format!("{output}:-"); let output_arg = format!("{output}:-");
let process = if coalesce { 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( Process::run(
"magick", "magick",
&[ &[
@ -68,6 +107,20 @@ async fn convert(
&output_arg, &output_arg,
], ],
)? )?
}
} else if let Some(quality) = quality {
Process::run(
"magick",
&[
"convert",
"-strip",
"-auto-orient",
&input_arg,
"-quality",
&quality.to_string(),
&output_arg,
],
)?
} else { } else {
Process::run( Process::run(
"magick", "magick",