feat: add ffmpeg plugin and update plugin interface to support temporary directories and context-dependent metrics
Release / check-release (push) Successful in 15s
Release / build_gnu (push) Skipped
Release / build_musl (push) Skipped
Release / build_windows (push) Skipped
Release / package_gnu (deb) (push) Skipped
Release / package_gnu (rpm) (push) Skipped
Release / package_musl (deb) (push) Skipped
Release / package_musl (rpm) (push) Skipped
Release / publish-release (push) Skipped
CI / Test (push) Successful in 1m31s
CI / Build Linux (push) Successful in 1m47s
Release / check-release (push) Successful in 15s
Release / build_gnu (push) Skipped
Release / build_musl (push) Skipped
Release / build_windows (push) Skipped
Release / package_gnu (deb) (push) Skipped
Release / package_gnu (rpm) (push) Skipped
Release / package_musl (deb) (push) Skipped
Release / package_musl (rpm) (push) Skipped
Release / publish-release (push) Skipped
CI / Test (push) Successful in 1m31s
CI / Build Linux (push) Successful in 1m47s
This commit is contained in:
1 parent
e46696c521
commit
b97a108d1e
10 files changed
+705
-134
No files matched your search
@@ -1,3 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -17,38 +31,36 @@ fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
let dest_path = Path::new(&out_dir).join("plugins_gen.rs");
|
let dest_path = Path::new(&out_dir).join("plugins_gen.rs");
|
||||||
|
|
||||||
// Read the plugins directory
|
// Read the plugins directory
|
||||||
let plugins_dir = Path::new("plugins");
|
let plugins_dir = Path::new("plugins");
|
||||||
|
|
||||||
let mut plugins_code = String::new();
|
let mut plugins_code = String::new();
|
||||||
let mut registry_add_code = String::new();
|
let mut registry_add_code = String::new();
|
||||||
|
|
||||||
if plugins_dir.exists() {
|
if plugins_dir.exists() {
|
||||||
for entry in fs::read_dir(plugins_dir).unwrap() {
|
for entry in fs::read_dir(plugins_dir).unwrap() {
|
||||||
let entry = entry.unwrap();
|
let entry = entry.unwrap();
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
|
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
|
||||||
let stem = path.file_stem().unwrap().to_str().unwrap();
|
let stem = path.file_stem().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
// We'll use include! macro to include the file content directly.
|
// We'll use include! macro to include the file content directly.
|
||||||
// However, `include!` requires a valid path. The generated file is in OUT_DIR,
|
// However, `include!` requires a valid path. The generated file is in OUT_DIR,
|
||||||
// so we need an absolute path or relative to the generated file.
|
// so we need an absolute path or relative to the generated file.
|
||||||
let abs_path = path.canonicalize().unwrap();
|
let abs_path = path.canonicalize().unwrap();
|
||||||
let abs_path_str = abs_path.to_str().unwrap();
|
let abs_path_str = abs_path.to_str().unwrap();
|
||||||
|
|
||||||
plugins_code.push_str(&format!(
|
plugins_code.push_str(&format!(
|
||||||
"pub mod {} {{\n include!({:?});\n}}\n",
|
"pub mod {} {{\n include!({:?});\n}}\n",
|
||||||
stem, abs_path_str
|
stem, abs_path_str
|
||||||
));
|
));
|
||||||
registry_add_code.push_str(&format!(
|
registry_add_code
|
||||||
"registry.push(Box::new({}::PluginImpl));\n",
|
.push_str(&format!("registry.push(Box::new({}::PluginImpl));\n", stem));
|
||||||
stem
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let full_code = format!(
|
let full_code = format!(
|
||||||
"
|
"
|
||||||
{}
|
{}
|
||||||
@@ -60,32 +72,32 @@ fn main() {
|
|||||||
",
|
",
|
||||||
plugins_code, registry_add_code
|
plugins_code, registry_add_code
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::write(&dest_path, full_code).unwrap();
|
fs::write(&dest_path, full_code).unwrap();
|
||||||
println!("cargo:rerun-if-changed=plugins");
|
println!("cargo:rerun-if-changed=plugins");
|
||||||
|
|
||||||
// Add build metadata for versioning
|
// Add build metadata for versioning
|
||||||
let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string());
|
let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string());
|
||||||
|
|
||||||
let build_time = run_command("date", &["-u", "+%Y-%m-%d %H:%M:%S UTC"])
|
let build_time = run_command("date", &["-u", "+%Y-%m-%d %H:%M:%S UTC"])
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
|
||||||
let git_commit = run_command("git", &["rev-parse", "--short", "HEAD"])
|
let git_commit = run_command("git", &["rev-parse", "--short", "HEAD"])
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
|
||||||
let target = env::var("TARGET").unwrap_or_else(|_| "Unknown".to_string());
|
let target = env::var("TARGET").unwrap_or_else(|_| "Unknown".to_string());
|
||||||
let profile = env::var("PROFILE").unwrap_or_else(|_| "Unknown".to_string());
|
let profile = env::var("PROFILE").unwrap_or_else(|_| "Unknown".to_string());
|
||||||
|
|
||||||
let rustc_path = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
|
let rustc_path = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
|
||||||
let rustc_version = run_command(&rustc_path, &["--version"])
|
let rustc_version = run_command(&rustc_path, &["--version"])
|
||||||
.map(|s| s.split('\n').next().unwrap_or("").to_string())
|
.map(|s| s.split('\n').next().unwrap_or("").to_string())
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
|
||||||
let version_string = format!(
|
let version_string = format!(
|
||||||
"{}\nbuild-time: {}\ncommit: {}\ntarget: {}\nprofile: {}\nrustc: {}",
|
"{}\nbuild-time: {}\ncommit: {}\ntarget: {}\nprofile: {}\nrustc: {}",
|
||||||
version, build_time, git_commit, target, profile, rustc_version
|
version, build_time, git_commit, target, profile, rustc_version
|
||||||
);
|
);
|
||||||
|
|
||||||
let version_path = Path::new(&out_dir).join("version.txt");
|
let version_path = Path::new(&out_dir).join("version.txt");
|
||||||
fs::write(&version_path, &version_string).unwrap();
|
fs::write(&version_path, &version_string).unwrap();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use crate::plugin::Plugin;
|
||||||
|
use rust_ffmpeg::FFmpegBuilder;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct PluginImpl;
|
||||||
|
|
||||||
|
impl Plugin for PluginImpl {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"ffmpeg_video"
|
||||||
|
}
|
||||||
|
fn from_formats(&self) -> Vec<&'static str> {
|
||||||
|
vec!["mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "gif"]
|
||||||
|
}
|
||||||
|
fn to_formats(&self) -> Vec<&'static str> {
|
||||||
|
vec!["mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "gif"]
|
||||||
|
}
|
||||||
|
fn familiarity(&self, _from: &str, to: &str) -> u8 {
|
||||||
|
match to {
|
||||||
|
"mp4" | "gif" => 255,
|
||||||
|
"mov" => 240,
|
||||||
|
"webm" => 220,
|
||||||
|
"mkv" => 180,
|
||||||
|
"wmv" => 160,
|
||||||
|
"flv" => 120,
|
||||||
|
"avi" => 100,
|
||||||
|
_ => 128,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn quality(&self, _from: &str, to: &str) -> u8 {
|
||||||
|
match to {
|
||||||
|
"mkv" => 250,
|
||||||
|
"mov" => 250,
|
||||||
|
"mp4" => 240,
|
||||||
|
"webm" => 210,
|
||||||
|
"wmv" => 200,
|
||||||
|
"flv" => 180,
|
||||||
|
"avi" => 150,
|
||||||
|
"gif" => 80,
|
||||||
|
_ => 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn speed(&self, _from: &str, to: &str) -> u8 {
|
||||||
|
match to {
|
||||||
|
"mp4" => 220,
|
||||||
|
"mov" => 220,
|
||||||
|
"mkv" => 200,
|
||||||
|
"flv" => 190,
|
||||||
|
"avi" => 180,
|
||||||
|
"wmv" => 170,
|
||||||
|
"gif" => 150,
|
||||||
|
"webm" => 100,
|
||||||
|
_ => 150,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
&self,
|
||||||
|
input: &[u8],
|
||||||
|
_from: &str,
|
||||||
|
to: &str,
|
||||||
|
temp_dir: &Path,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let mut temp_in = tempfile::Builder::new()
|
||||||
|
.suffix(&format!(".{}", _from))
|
||||||
|
.tempfile_in(temp_dir)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
temp_in.write_all(input).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let temp_out = tempfile::Builder::new()
|
||||||
|
.suffix(&format!(".{}", to))
|
||||||
|
.tempfile_in(temp_dir)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let temp_out_path = temp_out.into_temp_path();
|
||||||
|
|
||||||
|
let in_path = temp_in.path().to_path_buf();
|
||||||
|
|
||||||
|
let mut raw_args = vec![];
|
||||||
|
match to {
|
||||||
|
"mp4" | "mkv" | "mov" => {
|
||||||
|
raw_args.extend(vec![
|
||||||
|
"-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-pix_fmt", "yuv420p",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
"webm" => {
|
||||||
|
raw_args.extend(vec![
|
||||||
|
"-c:v",
|
||||||
|
"libvpx-vp9",
|
||||||
|
"-crf",
|
||||||
|
"30",
|
||||||
|
"-b:v",
|
||||||
|
"0",
|
||||||
|
"-c:a",
|
||||||
|
"libopus",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
"avi" => {
|
||||||
|
raw_args.extend(vec![
|
||||||
|
"-c:v",
|
||||||
|
"mpeg4",
|
||||||
|
"-vtag",
|
||||||
|
"xvid",
|
||||||
|
"-qscale:v",
|
||||||
|
"3",
|
||||||
|
"-c:a",
|
||||||
|
"libmp3lame",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
"gif" => {
|
||||||
|
raw_args.extend(vec!["-vf", "fps=15,scale=320:-1:flags=lanczos"]);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
raw_args.extend(vec!["-qscale:v", "3"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
|
||||||
|
rt.block_on(async {
|
||||||
|
FFmpegBuilder::new()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.input_path(in_path)
|
||||||
|
.output_path(temp_out_path.to_path_buf())
|
||||||
|
.raw_args(raw_args)
|
||||||
|
.overwrite()
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
std::fs::read(&temp_out_path).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-8
@@ -1,18 +1,51 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::plugin::Plugin;
|
use crate::plugin::Plugin;
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct PluginImpl;
|
pub struct PluginImpl;
|
||||||
|
|
||||||
impl Plugin for PluginImpl {
|
impl Plugin for PluginImpl {
|
||||||
fn name(&self) -> &'static str { "jpeg_to_png" }
|
fn name(&self) -> &'static str {
|
||||||
fn from_formats(&self) -> Vec<&'static str> { vec!["jpeg"] }
|
"jpeg_to_png"
|
||||||
fn to_formats(&self) -> Vec<&'static str> { vec!["png"] }
|
}
|
||||||
fn familiarity(&self) -> u8 { 255 }
|
fn from_formats(&self) -> Vec<&'static str> {
|
||||||
fn quality(&self) -> u8 { 255 }
|
vec!["jpeg"]
|
||||||
fn speed(&self) -> u8 { 200 }
|
}
|
||||||
|
fn to_formats(&self) -> Vec<&'static str> {
|
||||||
fn convert(&self, input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> {
|
vec!["png"]
|
||||||
|
}
|
||||||
|
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
255
|
||||||
|
}
|
||||||
|
fn quality(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
255
|
||||||
|
}
|
||||||
|
fn speed(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
200
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
&self,
|
||||||
|
input: &[u8],
|
||||||
|
_from: &str,
|
||||||
|
_to: &str,
|
||||||
|
_temp_dir: &Path,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
let img = image::load_from_memory_with_format(input, ImageFormat::Jpeg)
|
let img = image::load_from_memory_with_format(input, ImageFormat::Jpeg)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|||||||
+41
-8
@@ -1,18 +1,51 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::plugin::Plugin;
|
use crate::plugin::Plugin;
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct PluginImpl;
|
pub struct PluginImpl;
|
||||||
|
|
||||||
impl Plugin for PluginImpl {
|
impl Plugin for PluginImpl {
|
||||||
fn name(&self) -> &'static str { "png_to_jpeg" }
|
fn name(&self) -> &'static str {
|
||||||
fn from_formats(&self) -> Vec<&'static str> { vec!["png"] }
|
"png_to_jpeg"
|
||||||
fn to_formats(&self) -> Vec<&'static str> { vec!["jpeg"] }
|
}
|
||||||
fn familiarity(&self) -> u8 { 255 }
|
fn from_formats(&self) -> Vec<&'static str> {
|
||||||
fn quality(&self) -> u8 { 200 } // JPEG has some compression loss
|
vec!["png"]
|
||||||
fn speed(&self) -> u8 { 220 }
|
}
|
||||||
|
fn to_formats(&self) -> Vec<&'static str> {
|
||||||
fn convert(&self, input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> {
|
vec!["jpeg"]
|
||||||
|
}
|
||||||
|
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
255
|
||||||
|
}
|
||||||
|
fn quality(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
200
|
||||||
|
} // JPEG has some compression loss
|
||||||
|
fn speed(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
220
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
&self,
|
||||||
|
input: &[u8],
|
||||||
|
_from: &str,
|
||||||
|
_to: &str,
|
||||||
|
_temp_dir: &Path,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
let img = image::load_from_memory_with_format(input, ImageFormat::Png)
|
let img = image::load_from_memory_with_format(input, ImageFormat::Png)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|||||||
+51
-10
@@ -1,4 +1,19 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/version.txt"));
|
pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/version.txt"));
|
||||||
|
|
||||||
@@ -7,24 +22,49 @@ pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/version.txt"))
|
|||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[arg(short = 'v', action = clap::ArgAction::Count, help = "Output information about the conversion. Use -v for path taken, -vv for step info, -vvv for timing, and -vvvv for plugin logs.")]
|
#[arg(short = 'v', action = clap::ArgAction::Count, help = "Output information about the conversion. Use -v for path taken, -vv for step info, -vvv for timing, and -vvvv for plugin logs.")]
|
||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
|
|
||||||
#[arg(short, long, help = "Test the conversion process without actually converting the file.")]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Test the conversion process without actually converting the file."
|
||||||
|
)]
|
||||||
pub test: bool,
|
pub test: bool,
|
||||||
|
|
||||||
#[arg(short, long, help = "Output nothing to the console, silently fail on errors.")]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Output nothing to the console, silently fail on errors."
|
||||||
|
)]
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
|
|
||||||
#[arg(short = 'c', long, help = "Get rid of the warning message when not piping the file.")]
|
#[arg(
|
||||||
|
short = 'c',
|
||||||
|
long,
|
||||||
|
help = "Get rid of the warning message when not piping the file."
|
||||||
|
)]
|
||||||
pub write_to_console: bool,
|
pub write_to_console: bool,
|
||||||
|
|
||||||
#[arg(short, long, default_value = "fqs", help = "Set the priority of the conversion process (e.g. -p fqs means \"familiarity, speed, quality\").")]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value = "fqs",
|
||||||
|
help = "Set the priority of the conversion process (e.g. -p fqs means \"familiarity, speed, quality\")."
|
||||||
|
)]
|
||||||
pub priority: String,
|
pub priority: String,
|
||||||
|
|
||||||
#[arg(help = "The input file path.")]
|
#[arg(help = "The input file path.")]
|
||||||
pub input_path: String,
|
pub input_path: String,
|
||||||
|
|
||||||
#[arg(help = "The output file path (optional).")]
|
#[arg(help = "The output file path (optional).")]
|
||||||
pub output_path: Option<String>,
|
pub output_path: Option<String>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short = 'T',
|
||||||
|
long,
|
||||||
|
default_value = "/dev/shm",
|
||||||
|
help = "Directory to use for temporary files during conversion. Defaults to /dev/shm (RAM). Use a disk path for very large files."
|
||||||
|
)]
|
||||||
|
pub temp_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@@ -49,6 +89,7 @@ mod tests {
|
|||||||
priority: "fqs".to_string(),
|
priority: "fqs".to_string(),
|
||||||
input_path: "test".to_string(),
|
input_path: "test".to_string(),
|
||||||
output_path: None,
|
output_path: None,
|
||||||
|
temp_dir: std::path::PathBuf::from("/dev/shm"),
|
||||||
};
|
};
|
||||||
assert_eq!(args.verbosity_level(), $expected);
|
assert_eq!(args.verbosity_level(), $expected);
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-2
@@ -1,11 +1,37 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn identify_format(path: &str) -> Option<&'static str> {
|
pub fn identify_format(path: &str) -> Option<&'static str> {
|
||||||
let p = Path::new(path);
|
let p = Path::new(path);
|
||||||
let ext = p.extension().and_then(|s| s.to_str()).unwrap_or(path).to_lowercase();
|
let ext = p
|
||||||
|
.extension()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or(path)
|
||||||
|
.to_lowercase();
|
||||||
match ext.as_str() {
|
match ext.as_str() {
|
||||||
"jpg" | "jpeg" => Some("jpeg"),
|
"jpg" | "jpeg" => Some("jpeg"),
|
||||||
"png" => Some("png"),
|
"png" => Some("png"),
|
||||||
|
"mp4" => Some("mp4"),
|
||||||
|
"webm" => Some("webm"),
|
||||||
|
"mkv" => Some("mkv"),
|
||||||
|
"avi" => Some("avi"),
|
||||||
|
"mov" => Some("mov"),
|
||||||
|
"wmv" => Some("wmv"),
|
||||||
|
"flv" => Some("flv"),
|
||||||
|
"gif" => Some("gif"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +61,11 @@ mod tests {
|
|||||||
test_ident!(test_identify_format_10, "test.bmp", None);
|
test_ident!(test_identify_format_10, "test.bmp", None);
|
||||||
test_ident!(test_identify_format_11, "file.JPEG", Some("jpeg"));
|
test_ident!(test_identify_format_11, "file.JPEG", Some("jpeg"));
|
||||||
test_ident!(test_identify_format_12, "file.PNG", Some("png"));
|
test_ident!(test_identify_format_12, "file.PNG", Some("png"));
|
||||||
test_ident!(test_identify_format_13, "complex.file.name.jpg", Some("jpeg"));
|
test_ident!(
|
||||||
|
test_identify_format_13,
|
||||||
|
"complex.file.name.jpg",
|
||||||
|
Some("jpeg")
|
||||||
|
);
|
||||||
test_ident!(test_identify_format_14, ".hidden.png", Some("png"));
|
test_ident!(test_identify_format_14, ".hidden.png", Some("png"));
|
||||||
test_ident!(test_identify_format_15, "jpg", Some("jpeg")); // "jpg" passed without dot, treats as ext if no dot in path but path is "jpg", ext becomes "jpg"
|
test_ident!(test_identify_format_15, "jpg", Some("jpeg")); // "jpg" passed without dot, treats as ext if no dot in path but path is "jpg", ext becomes "jpg"
|
||||||
test_ident!(test_identify_format_16, "a.jpg.txt", None);
|
test_ident!(test_identify_format_16, "a.jpg.txt", None);
|
||||||
|
|||||||
+34
-13
@@ -1,3 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod identifier;
|
pub mod identifier;
|
||||||
pub mod pathfinder;
|
pub mod pathfinder;
|
||||||
@@ -14,7 +28,7 @@ use std::process;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
// Read input file
|
// Read input file
|
||||||
let input_bytes = match fs::read(&args.input_path) {
|
let input_bytes = match fs::read(&args.input_path) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
@@ -25,7 +39,7 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let from_format = match identifier::identify_format(&args.input_path) {
|
let from_format = match identifier::identify_format(&args.input_path) {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => {
|
None => {
|
||||||
@@ -35,7 +49,7 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let plugins = get_plugins();
|
let plugins = get_plugins();
|
||||||
|
|
||||||
let to_format = match &args.output_path {
|
let to_format = match &args.output_path {
|
||||||
@@ -61,24 +75,29 @@ fn main() {
|
|||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
eprintln!("Error: No output path provided, and no available conversions found.");
|
eprintln!(
|
||||||
|
"Error: No output path provided, and no available conversions found."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = match pathfinder::find_best_path(&plugins, from_format, to_format, &args.priority) {
|
let path = match pathfinder::find_best_path(&plugins, from_format, to_format, &args.priority) {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
eprintln!("Error: No conversion path found from {} to {}.", from_format, to_format);
|
eprintln!(
|
||||||
|
"Error: No conversion path found from {} to {}.",
|
||||||
|
from_format, to_format
|
||||||
|
);
|
||||||
}
|
}
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.test {
|
if args.test {
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
let path_str: Vec<_> = path.iter().map(|(p, _, _)| p.name()).collect();
|
let path_str: Vec<_> = path.iter().map(|(p, _, _)| p.name()).collect();
|
||||||
@@ -86,10 +105,10 @@ fn main() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let verbosity = args.verbosity_level();
|
let verbosity = args.verbosity_level();
|
||||||
let result = runner::run_conversion(&path, &input_bytes, verbosity);
|
let result = runner::run_conversion(&path, &input_bytes, verbosity, &args.temp_dir);
|
||||||
|
|
||||||
let output_bytes = match result {
|
let output_bytes = match result {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -99,7 +118,7 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(out_path) = &args.output_path {
|
if let Some(out_path) = &args.output_path {
|
||||||
if let Err(e) = fs::write(out_path, &output_bytes) {
|
if let Err(e) = fs::write(out_path, &output_bytes) {
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
@@ -111,10 +130,12 @@ fn main() {
|
|||||||
let is_piped = !io::stdout().is_terminal();
|
let is_piped = !io::stdout().is_terminal();
|
||||||
if !is_piped && !args.write_to_console {
|
if !is_piped && !args.write_to_console {
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
eprintln!("Warning: Outputting directly to console. Use -c to suppress this warning, or redirect to a file.");
|
eprintln!(
|
||||||
|
"Warning: Outputting directly to console. Use -c to suppress this warning, or redirect to a file."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
if let Err(e) = stdout.write_all(&output_bytes) {
|
if let Err(e) = stdout.write_all(&output_bytes) {
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
|
|||||||
+233
-55
@@ -1,3 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::plugin::Plugin;
|
use crate::plugin::Plugin;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -14,13 +28,13 @@ impl<'a> PathNode<'a> {
|
|||||||
pub fn get_path(&self) -> Vec<(&'a dyn Plugin, &'a str, &'a str)> {
|
pub fn get_path(&self) -> Vec<(&'a dyn Plugin, &'a str, &'a str)> {
|
||||||
let mut path = Vec::new();
|
let mut path = Vec::new();
|
||||||
path.push((self.plugin, self.from_format, self.to_format));
|
path.push((self.plugin, self.from_format, self.to_format));
|
||||||
|
|
||||||
let mut curr = self.prev.clone();
|
let mut curr = self.prev.clone();
|
||||||
while let Some(node) = curr {
|
while let Some(node) = curr {
|
||||||
path.push((node.plugin, node.from_format, node.to_format));
|
path.push((node.plugin, node.from_format, node.to_format));
|
||||||
curr = node.prev.clone();
|
curr = node.prev.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
path.reverse();
|
path.reverse();
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
@@ -42,22 +56,22 @@ pub fn find_best_path<'a>(
|
|||||||
// BFS to find paths with least amount of conversions
|
// BFS to find paths with least amount of conversions
|
||||||
let mut queue = VecDeque::new();
|
let mut queue = VecDeque::new();
|
||||||
queue.push_back((from_format, None::<Rc<PathNode<'a>>>));
|
queue.push_back((from_format, None::<Rc<PathNode<'a>>>));
|
||||||
|
|
||||||
let mut min_length = None;
|
let mut min_length = None;
|
||||||
let mut best_paths: Vec<Vec<(&'a dyn Plugin, &'a str, &'a str)>> = Vec::new();
|
let mut best_paths: Vec<Vec<(&'a dyn Plugin, &'a str, &'a str)>> = Vec::new();
|
||||||
|
|
||||||
let mut visited_depth = HashMap::new();
|
let mut visited_depth = HashMap::new();
|
||||||
visited_depth.insert(from_format, 0);
|
visited_depth.insert(from_format, 0);
|
||||||
|
|
||||||
while let Some((curr_format, prev_node)) = queue.pop_front() {
|
while let Some((curr_format, prev_node)) = queue.pop_front() {
|
||||||
let current_depth = visited_depth.get(curr_format).copied().unwrap_or(0);
|
let current_depth = visited_depth.get(curr_format).copied().unwrap_or(0);
|
||||||
|
|
||||||
if let Some(min_len) = min_length {
|
if let Some(min_len) = min_length {
|
||||||
if current_depth > min_len {
|
if current_depth > min_len {
|
||||||
break; // We've moved beyond the shortest paths
|
break; // We've moved beyond the shortest paths
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if curr_format == to_format && prev_node.is_some() {
|
if curr_format == to_format && prev_node.is_some() {
|
||||||
if min_length.is_none() {
|
if min_length.is_none() {
|
||||||
min_length = Some(current_depth);
|
min_length = Some(current_depth);
|
||||||
@@ -72,60 +86,85 @@ pub fn find_best_path<'a>(
|
|||||||
for plugin in neighbors {
|
for plugin in neighbors {
|
||||||
for &next_format in &plugin.to_formats() {
|
for &next_format in &plugin.to_formats() {
|
||||||
let next_depth = current_depth + 1;
|
let next_depth = current_depth + 1;
|
||||||
|
|
||||||
let prev_depth = visited_depth.get(next_format).copied().unwrap_or(usize::MAX);
|
let prev_depth = visited_depth
|
||||||
|
.get(next_format)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
if next_depth <= prev_depth {
|
if next_depth <= prev_depth {
|
||||||
visited_depth.insert(next_format, next_depth);
|
visited_depth.insert(next_format, next_depth);
|
||||||
|
|
||||||
let new_node = Rc::new(PathNode {
|
let new_node = Rc::new(PathNode {
|
||||||
plugin: *plugin,
|
plugin: *plugin,
|
||||||
from_format: curr_format,
|
from_format: curr_format,
|
||||||
to_format: next_format,
|
to_format: next_format,
|
||||||
prev: prev_node.clone(),
|
prev: prev_node.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
queue.push_back((next_format, Some(new_node)));
|
queue.push_back((next_format, Some(new_node)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if best_paths.is_empty() {
|
if best_paths.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate based on priority (maximize score)
|
// Evaluate based on priority (maximize score)
|
||||||
best_paths.into_iter().max_by(|path_a, path_b| {
|
best_paths
|
||||||
compare_paths(path_a, path_b, priority)
|
.into_iter()
|
||||||
})
|
.max_by(|path_a, path_b| compare_paths(path_a, path_b, priority))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compare_paths(path_a: &[(&dyn Plugin, &str, &str)], path_b: &[(&dyn Plugin, &str, &str)], priority: &str) -> std::cmp::Ordering {
|
fn compare_paths(
|
||||||
|
path_a: &[(&dyn Plugin, &str, &str)],
|
||||||
|
path_b: &[(&dyn Plugin, &str, &str)],
|
||||||
|
priority: &str,
|
||||||
|
) -> std::cmp::Ordering {
|
||||||
for ch in priority.chars() {
|
for ch in priority.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
'f' | 'F' => {
|
'f' | 'F' => {
|
||||||
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.familiarity() as u32).sum();
|
let score_a: u32 = path_a
|
||||||
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.familiarity() as u32).sum();
|
.iter()
|
||||||
|
.map(|(p, from, to)| p.familiarity(*from, *to) as u32)
|
||||||
|
.sum();
|
||||||
|
let score_b: u32 = path_b
|
||||||
|
.iter()
|
||||||
|
.map(|(p, from, to)| p.familiarity(*from, *to) as u32)
|
||||||
|
.sum();
|
||||||
if score_a != score_b {
|
if score_a != score_b {
|
||||||
return score_a.cmp(&score_b);
|
return score_a.cmp(&score_b);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
'q' | 'Q' => {
|
'q' | 'Q' => {
|
||||||
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.quality() as u32).sum();
|
let score_a: u32 = path_a
|
||||||
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.quality() as u32).sum();
|
.iter()
|
||||||
|
.map(|(p, from, to)| p.quality(*from, *to) as u32)
|
||||||
|
.sum();
|
||||||
|
let score_b: u32 = path_b
|
||||||
|
.iter()
|
||||||
|
.map(|(p, from, to)| p.quality(*from, *to) as u32)
|
||||||
|
.sum();
|
||||||
if score_a != score_b {
|
if score_a != score_b {
|
||||||
return score_a.cmp(&score_b);
|
return score_a.cmp(&score_b);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
's' | 'S' => {
|
's' | 'S' => {
|
||||||
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.speed() as u32).sum();
|
let score_a: u32 = path_a
|
||||||
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.speed() as u32).sum();
|
.iter()
|
||||||
|
.map(|(p, from, to)| p.speed(*from, *to) as u32)
|
||||||
|
.sum();
|
||||||
|
let score_b: u32 = path_b
|
||||||
|
.iter()
|
||||||
|
.map(|(p, from, to)| p.speed(*from, *to) as u32)
|
||||||
|
.sum();
|
||||||
if score_a != score_b {
|
if score_a != score_b {
|
||||||
return score_a.cmp(&score_b);
|
return score_a.cmp(&score_b);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,23 +185,64 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for MockPlugin {
|
impl Plugin for MockPlugin {
|
||||||
fn name(&self) -> &'static str { self.name }
|
fn name(&self) -> &'static str {
|
||||||
fn from_formats(&self) -> Vec<&'static str> { self.from.clone() }
|
self.name
|
||||||
fn to_formats(&self) -> Vec<&'static str> { self.to.clone() }
|
}
|
||||||
fn familiarity(&self) -> u8 { self.f }
|
fn from_formats(&self) -> Vec<&'static str> {
|
||||||
fn quality(&self) -> u8 { self.q }
|
self.from.clone()
|
||||||
fn speed(&self) -> u8 { self.s }
|
}
|
||||||
fn convert(&self, _input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> { Ok(vec![]) }
|
fn to_formats(&self) -> Vec<&'static str> {
|
||||||
|
self.to.clone()
|
||||||
|
}
|
||||||
|
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
self.f
|
||||||
|
}
|
||||||
|
fn quality(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
self.q
|
||||||
|
}
|
||||||
|
fn speed(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
self.s
|
||||||
|
}
|
||||||
|
fn convert(
|
||||||
|
&self,
|
||||||
|
_input: &[u8],
|
||||||
|
_from: &str,
|
||||||
|
_to: &str,
|
||||||
|
_temp_dir: &std::path::Path,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_shortest_path() {
|
fn test_shortest_path() {
|
||||||
let plugins: Vec<Box<dyn Plugin>> = vec![
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
||||||
Box::new(MockPlugin { name: "A", from: vec!["jpeg"], to: vec!["png"], f: 10, q: 10, s: 10 }),
|
Box::new(MockPlugin {
|
||||||
Box::new(MockPlugin { name: "B", from: vec!["jpeg"], to: vec!["bmp"], f: 10, q: 10, s: 10 }),
|
name: "A",
|
||||||
Box::new(MockPlugin { name: "C", from: vec!["bmp"], to: vec!["png"], f: 10, q: 10, s: 10 }),
|
from: vec!["jpeg"],
|
||||||
|
to: vec!["png"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "B",
|
||||||
|
from: vec!["jpeg"],
|
||||||
|
to: vec!["bmp"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "C",
|
||||||
|
from: vec!["bmp"],
|
||||||
|
to: vec!["png"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
|
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
|
||||||
assert_eq!(path.len(), 1);
|
assert_eq!(path.len(), 1);
|
||||||
assert_eq!(path[0].0.name(), "A");
|
assert_eq!(path[0].0.name(), "A");
|
||||||
@@ -171,13 +251,27 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_priority_tie_break() {
|
fn test_priority_tie_break() {
|
||||||
let plugins: Vec<Box<dyn Plugin>> = vec![
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
||||||
Box::new(MockPlugin { name: "A", from: vec!["jpeg"], to: vec!["png"], f: 10, q: 50, s: 10 }),
|
Box::new(MockPlugin {
|
||||||
Box::new(MockPlugin { name: "B", from: vec!["jpeg"], to: vec!["png"], f: 50, q: 10, s: 10 }),
|
name: "A",
|
||||||
|
from: vec!["jpeg"],
|
||||||
|
to: vec!["png"],
|
||||||
|
f: 10,
|
||||||
|
q: 50,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "B",
|
||||||
|
from: vec!["jpeg"],
|
||||||
|
to: vec!["png"],
|
||||||
|
f: 50,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
|
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
|
||||||
assert_eq!(path[0].0.name(), "B");
|
assert_eq!(path[0].0.name(), "B");
|
||||||
|
|
||||||
let path = find_best_path(&plugins, "jpeg", "png", "qfs").unwrap();
|
let path = find_best_path(&plugins, "jpeg", "png", "qfs").unwrap();
|
||||||
assert_eq!(path[0].0.name(), "A");
|
assert_eq!(path[0].0.name(), "A");
|
||||||
}
|
}
|
||||||
@@ -187,15 +281,64 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
let plugins: Vec<Box<dyn Plugin>> = vec![
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
||||||
Box::new(MockPlugin { name: "A", from: vec!["a"], to: vec!["b"], f: 10, q: 10, s: 10 }),
|
Box::new(MockPlugin {
|
||||||
Box::new(MockPlugin { name: "B", from: vec!["b"], to: vec!["c"], f: 20, q: 10, s: 10 }),
|
name: "A",
|
||||||
Box::new(MockPlugin { name: "C", from: vec!["a"], to: vec!["c"], f: 5, q: 10, s: 10 }),
|
from: vec!["a"],
|
||||||
Box::new(MockPlugin { name: "D", from: vec!["a"], to: vec!["d"], f: 10, q: 20, s: 10 }),
|
to: vec!["b"],
|
||||||
Box::new(MockPlugin { name: "E", from: vec!["d"], to: vec!["c"], f: 10, q: 20, s: 10 }),
|
f: 10,
|
||||||
Box::new(MockPlugin { name: "F", from: vec!["a"], to: vec!["b"], f: 50, q: 5, s: 5 }), // High familiarity, low q/s
|
q: 10,
|
||||||
Box::new(MockPlugin { name: "G", from: vec!["c"], to: vec!["e"], f: 10, q: 10, s: 50 }),
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "B",
|
||||||
|
from: vec!["b"],
|
||||||
|
to: vec!["c"],
|
||||||
|
f: 20,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "C",
|
||||||
|
from: vec!["a"],
|
||||||
|
to: vec!["c"],
|
||||||
|
f: 5,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "D",
|
||||||
|
from: vec!["a"],
|
||||||
|
to: vec!["d"],
|
||||||
|
f: 10,
|
||||||
|
q: 20,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "E",
|
||||||
|
from: vec!["d"],
|
||||||
|
to: vec!["c"],
|
||||||
|
f: 10,
|
||||||
|
q: 20,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "F",
|
||||||
|
from: vec!["a"],
|
||||||
|
to: vec!["b"],
|
||||||
|
f: 50,
|
||||||
|
q: 5,
|
||||||
|
s: 5,
|
||||||
|
}), // High familiarity, low q/s
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "G",
|
||||||
|
from: vec!["c"],
|
||||||
|
to: vec!["e"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 50,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let path = find_best_path(&plugins, $from, $to, $priority);
|
let path = find_best_path(&plugins, $from, $to, $priority);
|
||||||
if $expected_len == 0 {
|
if $expected_len == 0 {
|
||||||
assert!(path.is_none());
|
assert!(path.is_none());
|
||||||
@@ -224,11 +367,46 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
let plugins: Vec<Box<dyn Plugin>> = vec![
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
||||||
Box::new(MockPlugin { name: "1", from: vec!["1"], to: vec!["2"], f: 10, q: 10, s: 10 }),
|
Box::new(MockPlugin {
|
||||||
Box::new(MockPlugin { name: "2", from: vec!["2"], to: vec!["3"], f: 10, q: 10, s: 10 }),
|
name: "1",
|
||||||
Box::new(MockPlugin { name: "3", from: vec!["3"], to: vec!["4"], f: 10, q: 10, s: 10 }),
|
from: vec!["1"],
|
||||||
Box::new(MockPlugin { name: "4", from: vec!["4"], to: vec!["5"], f: 10, q: 10, s: 10 }),
|
to: vec!["2"],
|
||||||
Box::new(MockPlugin { name: "5", from: vec!["5"], to: vec!["6"], f: 10, q: 10, s: 10 }),
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "2",
|
||||||
|
from: vec!["2"],
|
||||||
|
to: vec!["3"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "3",
|
||||||
|
from: vec!["3"],
|
||||||
|
to: vec!["4"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "4",
|
||||||
|
from: vec!["4"],
|
||||||
|
to: vec!["5"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
|
Box::new(MockPlugin {
|
||||||
|
name: "5",
|
||||||
|
from: vec!["5"],
|
||||||
|
to: vec!["6"],
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
let path = find_best_path(&plugins, $from, $to, $priority);
|
let path = find_best_path(&plugins, $from, $to, $priority);
|
||||||
if $expected_len == 0 {
|
if $expected_len == 0 {
|
||||||
@@ -239,7 +417,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test_pathfinder_2!(test_p2_1, "1", "2", "f", 1);
|
test_pathfinder_2!(test_p2_1, "1", "2", "f", 1);
|
||||||
test_pathfinder_2!(test_p2_2, "1", "3", "f", 2);
|
test_pathfinder_2!(test_p2_2, "1", "3", "f", 2);
|
||||||
test_pathfinder_2!(test_p2_3, "1", "4", "f", 3);
|
test_pathfinder_2!(test_p2_3, "1", "4", "f", 3);
|
||||||
|
|||||||
+29
-6
@@ -1,13 +1,36 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub trait Plugin: Send + Sync {
|
pub trait Plugin: Send + Sync {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
fn from_formats(&self) -> Vec<&'static str>;
|
fn from_formats(&self) -> Vec<&'static str>;
|
||||||
fn to_formats(&self) -> Vec<&'static str>;
|
fn to_formats(&self) -> Vec<&'static str>;
|
||||||
|
|
||||||
/// Score from 1 to 255. Higher is better.
|
/// Score from 1 to 255. Higher is better.
|
||||||
fn familiarity(&self) -> u8;
|
fn familiarity(&self, from_format: &str, to_format: &str) -> u8;
|
||||||
fn quality(&self) -> u8;
|
fn quality(&self, from_format: &str, to_format: &str) -> u8;
|
||||||
fn speed(&self) -> u8;
|
fn speed(&self, from_format: &str, to_format: &str) -> u8;
|
||||||
|
|
||||||
/// Converts the given input bytes from `from_format` to `to_format`.
|
/// Converts the given input bytes from `from_format` to `to_format`.
|
||||||
fn convert(&self, input: &[u8], from_format: &str, to_format: &str) -> Result<Vec<u8>, String>;
|
/// `temp_dir` is the directory to use for any intermediate scratch files.
|
||||||
|
fn convert(
|
||||||
|
&self,
|
||||||
|
input: &[u8],
|
||||||
|
from_format: &str,
|
||||||
|
to_format: &str,
|
||||||
|
temp_dir: &Path,
|
||||||
|
) -> Result<Vec<u8>, String>;
|
||||||
}
|
}
|
||||||
+68
-14
@@ -1,10 +1,26 @@
|
|||||||
|
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, version 3 exclusively.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::plugin::Plugin;
|
use crate::plugin::Plugin;
|
||||||
|
use std::path::Path;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
pub fn run_conversion(
|
pub fn run_conversion(
|
||||||
path: &[(&dyn Plugin, &str, &str)],
|
path: &[(&dyn Plugin, &str, &str)],
|
||||||
input: &[u8],
|
input: &[u8],
|
||||||
verbosity: u8,
|
verbosity: u8,
|
||||||
|
temp_dir: &Path,
|
||||||
) -> Result<Vec<u8>, String> {
|
) -> Result<Vec<u8>, String> {
|
||||||
if verbosity >= 1 {
|
if verbosity >= 1 {
|
||||||
let path_str: Vec<_> = path.iter().map(|(p, _, _)| p.name()).collect();
|
let path_str: Vec<_> = path.iter().map(|(p, _, _)| p.name()).collect();
|
||||||
@@ -15,22 +31,35 @@ pub fn run_conversion(
|
|||||||
|
|
||||||
for (plugin, from_format, to_format) in path {
|
for (plugin, from_format, to_format) in path {
|
||||||
if verbosity >= 2 {
|
if verbosity >= 2 {
|
||||||
eprintln!("Converting {} to {} using plugin {}...", from_format, to_format, plugin.name());
|
eprintln!(
|
||||||
|
"Converting {} to {} using plugin {}...",
|
||||||
|
from_format,
|
||||||
|
to_format,
|
||||||
|
plugin.name()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if verbosity >= 4 {
|
if verbosity >= 4 {
|
||||||
eprintln!("[Plugin Log] Running plugin: {}", plugin.name());
|
eprintln!("[Plugin Log] Running plugin: {}", plugin.name());
|
||||||
eprintln!("[Plugin Log] Source format: {}", from_format);
|
eprintln!("[Plugin Log] Source format: {}", from_format);
|
||||||
eprintln!("[Plugin Log] Target format: {}", to_format);
|
eprintln!("[Plugin Log] Target format: {}", to_format);
|
||||||
eprintln!("[Plugin Log] Metrics - Familiarity: {}, Quality: {}, Speed: {}", plugin.familiarity(), plugin.quality(), plugin.speed());
|
eprintln!(
|
||||||
|
"[Plugin Log] Metrics - Familiarity: {}, Quality: {}, Speed: {}",
|
||||||
|
plugin.familiarity(*from_format, *to_format),
|
||||||
|
plugin.quality(*from_format, *to_format),
|
||||||
|
plugin.speed(*from_format, *to_format)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
current_data = plugin.convert(¤t_data, from_format, to_format)?;
|
current_data = plugin.convert(¤t_data, from_format, to_format, temp_dir)?;
|
||||||
let elapsed = start_time.elapsed();
|
let elapsed = start_time.elapsed();
|
||||||
|
|
||||||
if verbosity >= 3 {
|
if verbosity >= 3 {
|
||||||
eprintln!("Step '{} -> {}' completed in {:.2?}", from_format, to_format, elapsed);
|
eprintln!(
|
||||||
|
"Step '{} -> {}' completed in {:.2?}",
|
||||||
|
from_format, to_format, elapsed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,13 +79,31 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for MockPlugin {
|
impl Plugin for MockPlugin {
|
||||||
fn name(&self) -> &'static str { self.name }
|
fn name(&self) -> &'static str {
|
||||||
fn from_formats(&self) -> Vec<&'static str> { vec!["a"] }
|
self.name
|
||||||
fn to_formats(&self) -> Vec<&'static str> { vec!["b"] }
|
}
|
||||||
fn familiarity(&self) -> u8 { self.f }
|
fn from_formats(&self) -> Vec<&'static str> {
|
||||||
fn quality(&self) -> u8 { self.q }
|
vec!["a"]
|
||||||
fn speed(&self) -> u8 { self.s }
|
}
|
||||||
fn convert(&self, input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> {
|
fn to_formats(&self) -> Vec<&'static str> {
|
||||||
|
vec!["b"]
|
||||||
|
}
|
||||||
|
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
self.f
|
||||||
|
}
|
||||||
|
fn quality(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
self.q
|
||||||
|
}
|
||||||
|
fn speed(&self, _from: &str, _to: &str) -> u8 {
|
||||||
|
self.s
|
||||||
|
}
|
||||||
|
fn convert(
|
||||||
|
&self,
|
||||||
|
input: &[u8],
|
||||||
|
_from: &str,
|
||||||
|
_to: &str,
|
||||||
|
_temp_dir: &Path,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
if self.fail {
|
if self.fail {
|
||||||
return Err("Failed".to_string());
|
return Err("Failed".to_string());
|
||||||
}
|
}
|
||||||
@@ -70,11 +117,18 @@ mod tests {
|
|||||||
($name:ident, $verbosity:expr, $fail:expr, $expected_res:expr, $expected_len:expr) => {
|
($name:ident, $verbosity:expr, $fail:expr, $expected_res:expr, $expected_len:expr) => {
|
||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
let plugin = MockPlugin { name: "mock", f: 10, q: 10, s: 10, fail: $fail };
|
let plugin = MockPlugin {
|
||||||
|
name: "mock",
|
||||||
|
f: 10,
|
||||||
|
q: 10,
|
||||||
|
s: 10,
|
||||||
|
fail: $fail,
|
||||||
|
};
|
||||||
let path: Vec<(&dyn Plugin, &str, &str)> = vec![(&plugin, "a", "b")];
|
let path: Vec<(&dyn Plugin, &str, &str)> = vec![(&plugin, "a", "b")];
|
||||||
let input = vec![0];
|
let input = vec![0];
|
||||||
|
|
||||||
let result = run_conversion(&path, &input, $verbosity);
|
let result =
|
||||||
|
run_conversion(&path, &input, $verbosity, std::path::Path::new("/dev/shm"));
|
||||||
assert_eq!(result.is_ok(), $expected_res);
|
assert_eq!(result.is_ok(), $expected_res);
|
||||||
if let Ok(res) = result {
|
if let Ok(res) = result {
|
||||||
assert_eq!(res.len(), $expected_len);
|
assert_eq!(res.len(), $expected_len);
|
||||||
|
|||||||
Reference in new issue
Block a user