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:
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
|
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"
|
||||||
|
|
158
pict-rs.toml
158
pict-rs.toml
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue