mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 11:21:24 +00:00
Attempt adding quality settings to pict-rs
This commit is contained in:
parent
a70dbe5c57
commit
b1bbc6b159
9 changed files with 633 additions and 23 deletions
|
@ -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"
|
||||
|
|
158
pict-rs.toml
158
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<ImageFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
quality: Option<ImageQuality>,
|
||||
}
|
||||
|
||||
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<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 {
|
||||
Some(self)
|
||||
|
@ -437,6 +514,8 @@ struct Animation {
|
|||
max_file_size: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<AnimationFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
quality: Option<AnimationQuality>,
|
||||
}
|
||||
|
||||
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<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 {
|
||||
Some(self)
|
||||
|
@ -477,6 +580,8 @@ struct Video {
|
|||
video_codec: Option<VideoCodec>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
audio_codec: Option<AudioCodec>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
quality: Option<VideoQuality>,
|
||||
}
|
||||
|
||||
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<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 {
|
||||
Some(self)
|
||||
|
@ -609,6 +748,31 @@ struct Run {
|
|||
/// Enforce a specific format for uploaded images
|
||||
#[arg(long)]
|
||||
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
|
||||
#[arg(long)]
|
||||
|
@ -628,6 +792,21 @@ struct Run {
|
|||
/// Enforce a specific format for uploaded animations
|
||||
#[arg(long)]
|
||||
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
|
||||
#[arg(long)]
|
||||
|
@ -656,6 +835,42 @@ struct Run {
|
|||
/// Enforce a specific audio codec for uploaded videos
|
||||
#[arg(long)]
|
||||
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
|
||||
#[arg(long)]
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -181,6 +181,42 @@ pub(crate) struct Image {
|
|||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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)]
|
||||
|
@ -197,6 +233,35 @@ pub(crate) struct Animation {
|
|||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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)]
|
||||
|
@ -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<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 {
|
||||
pub(crate) fn preprocess_steps(&self) -> Option<&[(String, String)]> {
|
||||
static PREPROCESS_STEPS: OnceCell<Vec<(String, String)>> = OnceCell::new();
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
pub(super) async fn transcode_bytes(
|
||||
input_format: VideoFormat,
|
||||
output_format: OutputVideoFormat,
|
||||
crf: u8,
|
||||
bytes: Bytes,
|
||||
) -> Result<impl AsyncRead + Unpin, FfMpegError> {
|
||||
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,
|
||||
|
|
|
@ -10,17 +10,33 @@ use crate::{
|
|||
pub(super) async fn convert_image(
|
||||
input: ImageFormat,
|
||||
output: ImageFormat,
|
||||
quality: Option<u8>,
|
||||
bytes: Bytes,
|
||||
) -> 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(
|
||||
input: AnimationFormat,
|
||||
output: AnimationFormat,
|
||||
quality: Option<u8>,
|
||||
bytes: Bytes,
|
||||
) -> 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(
|
||||
|
@ -28,13 +44,21 @@ pub(super) async fn convert_video(
|
|||
output: OutputVideoFormat,
|
||||
bytes: Bytes,
|
||||
) -> 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(
|
||||
input: &'static str,
|
||||
output: &'static str,
|
||||
coalesce: bool,
|
||||
quality: Option<u8>,
|
||||
bytes: Bytes,
|
||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||
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,
|
||||
],
|
||||
)?
|
||||
|
|
Loading…
Reference in a new issue