diff --git a/src/download.rs b/src/download.rs index 6817d97..1c76120 100644 --- a/src/download.rs +++ b/src/download.rs @@ -318,6 +318,10 @@ pub async fn download_artwork( if helper.force_yuv420p() { options.set("force_yuv420p", "1", None)?; } + let profile = helper.x264_profile(); + if !profile.is_auto() { + options.set("profile", profile.as_str(), None)?; + } let frames = UgoiraFrames::from_json(&ugoira_data["frames"])?; let output_file_name = base.join(format!("{}.mp4", id)); convert_ugoira_to_mp4( diff --git a/src/opthelper.rs b/src/opthelper.rs index 24564d1..9106b87 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -15,6 +15,8 @@ use crate::server::cors::parse_cors_entries; #[cfg(feature = "server")] use crate::server::cors::CorsEntry; use crate::settings::SettingStore; +#[cfg(feature = "ugoira")] +use crate::ugoira::X264Profile; use std::convert::TryFrom; #[cfg(feature = "server")] use std::net::IpAddr; @@ -390,6 +392,22 @@ impl OptHelper { } false } + + #[cfg(feature = "ugoira")] + /// Return the x264 profile when converting ugoira(GIF) to video. + pub fn x264_profile(&self) -> X264Profile { + match self.opt.get_ref().x264_profile { + Some(r) => { + return r; + } + None => {} + } + if self.settings.get_ref().have("x264-profile") { + let v = self.settings.get_ref().get("server").unwrap(); + return X264Profile::from_str(v.as_str().unwrap()).unwrap(); + } + X264Profile::default() + } } impl Default for OptHelper { diff --git a/src/opts.rs b/src/opts.rs index c9dc0a2..bb2f232 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -3,6 +3,8 @@ use crate::gettext; use crate::list::NonTailList; use crate::pixiv_link::PixivID; use crate::retry_interval::parse_retry_interval_from_str; +#[cfg(feature = "ugoira")] +use crate::ugoira::X264Profile; use crate::utils::check_file_exists; use crate::utils::get_exe_path_else_current; use getopts::HasArg; @@ -101,6 +103,9 @@ pub struct CommandOpts { #[cfg(feature = "ugoira")] /// Whether to force yuv420p as output pixel format when converting ugoira(GIF) to video. pub force_yuv420p: Option, + #[cfg(feature = "ugoira")] + /// The x264 profile when converting ugoira(GIF) to video. + pub x264_profile: Option, } impl CommandOpts { @@ -134,6 +139,8 @@ impl CommandOpts { max_download_post_tasks: None, #[cfg(feature = "ugoira")] force_yuv420p: None, + #[cfg(feature = "ugoira")] + x264_profile: None, } } @@ -286,6 +293,19 @@ pub fn parse_nonempty_usize>(s: Option) -> Result } } +#[cfg(feature = "ugoira")] +pub fn parse_x264_profile>( + s: Option, +) -> Result, &'static str> { + match s { + Some(s) => { + let s = s.as_ref(); + Ok(Some(X264Profile::from_str(s)?)) + } + None => Ok(Some(X264Profile::default())), + } +} + /// Parse optional option /// * `opts` - The result of options. See [getopts::Matches]. /// * `key` - The key of the option. @@ -475,6 +495,21 @@ pub fn parse_cmd() -> Option { HasArg::Maybe, getopts::Occur::Optional, ); + #[cfg(feature = "ugoira")] + opts.opt( + "", + "x264-profile", + format!( + "{} ({} {})", + gettext("The x264 profile when converting ugoira(GIF) to video."), + gettext("Default:"), + "auto" + ) + .as_str(), + "PROFILE", + HasArg::Maybe, + getopts::Occur::Optional, + ); let result = match opts.parse(&argv[1..]) { Ok(m) => m, Err(err) => { @@ -739,6 +774,25 @@ pub fn parse_cmd() -> Option { return None; } } + #[cfg(feature = "ugoira")] + match parse_optional_opt( + &result, + "x264-profile", + X264Profile::default(), + parse_x264_profile, + ) { + Ok(r) => re.as_mut().unwrap().x264_profile = r, + Err(e) => { + println!( + "{} {}", + gettext("Failed to parse :") + .replace("", "x264-profile") + .as_str(), + e + ); + return None; + } + } re } diff --git a/src/settings_list.rs b/src/settings_list.rs index 520ffe0..ff00731 100644 --- a/src/settings_list.rs +++ b/src/settings_list.rs @@ -9,10 +9,12 @@ use crate::opt::proxy::check_proxy; use crate::opt::size::parse_u32_size; #[cfg(feature = "server")] use crate::server::cors::parse_cors_entries; +#[cfg(feature = "ugoira")] +use crate::ugoira::X264Profile; use json::JsonValue; #[cfg(feature = "server")] use std::net::SocketAddr; -#[cfg(feature = "server")] +#[cfg(any(feature = "server", feature = "ugoira"))] use std::str::FromStr; pub fn get_settings_list() -> Vec { @@ -43,8 +45,10 @@ pub fn get_settings_list() -> Vec { SettingDes::new("max-download-tasks", gettext("The maximum number of tasks to download files at the same time."), JsonValueType::Number, Some(check_nozero_usize)).unwrap(), SettingDes::new("download-multiple-posts", gettext("Download multiple posts/artworks at the same time."), JsonValueType::Boolean, None).unwrap(), SettingDes::new("max-download-post-tasks", gettext("The maximum number of tasks to download posts/artworks at the same time."), JsonValueType::Number, Some(check_nozero_usize)).unwrap(), - #[cfg(feature = "ugora")] + #[cfg(feature = "ugoira")] SettingDes::new("force-yuv420p", gettext("Force yuv420p as output pixel format when converting ugoira(GIF) to video."), JsonValueType::Boolean, None).unwrap(), + #[cfg(feature = "ugoira")] + SettingDes::new("x264-profile", gettext("The x264 profile when converting ugoira(GIF) to video."), JsonValueType::Str, Some(check_x264_profile)).unwrap(), ] } @@ -100,6 +104,14 @@ fn check_user_or_not(obj: &JsonValue) -> bool { r.is_ok() } +#[cfg(feature = "ugoira")] +fn check_x264_profile(obj: &JsonValue) -> bool { + match obj.as_str() { + Some(profile) => X264Profile::from_str(profile).is_ok(), + None => false, + } +} + #[test] fn test_get_settings_list() { get_settings_list(); diff --git a/src/ugoira.rs b/src/ugoira.rs index 02bdc9a..36dc99c 100644 --- a/src/ugoira.rs +++ b/src/ugoira.rs @@ -8,6 +8,7 @@ use crate::ext::rawhandle::ToRawHandle; use crate::ext::try_err::TryErr; use crate::gettext; use std::convert::AsRef; +use std::convert::TryFrom; use std::default::Default; use std::ffi::CStr; use std::ffi::OsStr; @@ -22,6 +23,7 @@ use std::os::raw::c_int; use std::os::raw::c_void; #[cfg(test)] use std::path::Path; +use std::str::FromStr; use std::str::Utf8Error; const UGOIRA_OK: c_int = _ugoira::UGOIRA_OK as c_int; @@ -369,6 +371,80 @@ impl ToRawHandle<_ugoira::AVDictionary> for AVDict { } } +#[derive(Clone, Copy, Debug)] +/// H.264 profile +pub enum X264Profile { + /// Selected by x264. + Auto, + /// No interlaced, No lossless. + Baseline, + /// No lossless. + Main, + /// No lossless. + High, + /// No lossless. Support for bit depth 8-10. + High10, + /// No lossless. Support for bit depth 8-10. Support for 4:2:0/4:2:2 chroma subsampling. + High422, + /// No lossless. Support for bit depth 8-10. Support for 4:2:0/4:2:2/4:4:4 chroma subsampling. + High444, +} + +impl X264Profile { + pub fn as_str(&self) -> &'static str { + match self { + X264Profile::Auto => "", + X264Profile::Baseline => "baseline", + X264Profile::Main => "main", + X264Profile::High => "high", + X264Profile::High10 => "high10", + X264Profile::High422 => "high422", + X264Profile::High444 => "high444", + } + } + + #[inline] + pub fn is_auto(&self) -> bool { + matches!(self, X264Profile::Auto) + } +} + +impl AsRef for X264Profile { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Default for X264Profile { + fn default() -> Self { + X264Profile::Auto + } +} + +impl FromStr for X264Profile { + type Err = &'static str; + fn from_str(s: &str) -> Result { + let s = s.to_ascii_lowercase(); + match s.as_str() { + "auto" => Ok(X264Profile::Auto), + "baseline" => Ok(X264Profile::Baseline), + "main" => Ok(X264Profile::Main), + "high" => Ok(X264Profile::High), + "high10" => Ok(X264Profile::High10), + "high422" => Ok(X264Profile::High422), + "high444" => Ok(X264Profile::High444), + _ => Err(gettext("Unknown H.264 profile.")), + } + } +} + +impl TryFrom<&str> for X264Profile { + type Error = &'static str; + fn try_from(s: &str) -> Result { + X264Profile::from_str(s) + } +} + pub fn convert_ugoira_to_mp4< S: AsRef + ?Sized, D: AsRef + ?Sized,