use std::path::{Path, PathBuf};
use outline::{MaskPipeline, Outline};
use crate::cli::{
AlphaFromArg, CliMaskProcessingRequest, GlobalOptions, MaskExportSource, MaskProcessingArgs,
MaskSourceArg,
};
fn resolve_model_path(global: &GlobalOptions) -> PathBuf {
resolve_model_path_impl(global, default_cached_model_path().as_deref())
}
#[cfg(feature = "fetch-model")]
fn default_cached_model_path() -> Option<PathBuf> {
Some(crate::model_fetch::default_model_cache_path())
}
#[cfg(not(feature = "fetch-model"))]
fn default_cached_model_path() -> Option<PathBuf> {
None
}
fn resolve_model_path_impl(global: &GlobalOptions, cached: Option<&Path>) -> PathBuf {
if let Some(model) = global.model.clone() {
return model;
}
if let Some(cached) = cached.filter(|p| p.is_file()) {
eprintln!("Using cached model at {}", cached.display());
return cached.to_path_buf();
}
PathBuf::from(outline::DEFAULT_MODEL_PATH)
}
pub fn build_outline(global: &GlobalOptions) -> Outline {
#[cfg(all(feature = "backend-rten", not(feature = "backend-ort")))]
if global.intra_threads.is_some() {
eprintln!(
"Warning: --intra-threads is only supported by the ORT backend and will be ignored by the current backend."
);
}
let outline = Outline::new(resolve_model_path(global))
.with_input_resize_filter(global.input_resample_filter.into())
.with_output_resize_filter(global.output_resample_filter.into())
.with_intra_threads(global.intra_threads);
if let Some(size) = global.model_input_size {
outline.with_model_input_size(size.height(), size.width())
} else {
outline
}
}
pub fn derive_variant_path(input: &Path, suffix: &str, extension: &str) -> PathBuf {
let mut derived = input.to_path_buf();
let stem = input
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| suffix.to_string());
let filename = format!("{}-{}.{}", stem, suffix, extension);
derived.set_file_name(filename);
derived
}
pub fn resolve_export_path(
opt: &Option<Option<PathBuf>>,
input: &Path,
suffix: &str,
) -> Option<PathBuf> {
opt.as_ref().map(|inner| {
inner
.clone()
.unwrap_or_else(|| derive_variant_path(input, suffix, "png"))
})
}
pub fn derive_svg_path(input: &Path) -> PathBuf {
let mut path = input.to_path_buf();
path.set_extension("svg");
path
}
pub fn processing_requested(args: &MaskProcessingArgs) -> bool {
!CliMaskProcessingRequest::from_args(args).is_empty()
}
pub fn mask_pipeline_from_args(args: &MaskProcessingArgs) -> MaskPipeline {
CliMaskProcessingRequest::from_args(args).to_pipeline()
}
pub fn resolve_alpha_source(requested: AlphaFromArg, processing_requested: bool) -> AlphaFromArg {
match requested {
AlphaFromArg::Auto => {
if processing_requested {
AlphaFromArg::Processed
} else {
AlphaFromArg::Raw
}
}
other => other,
}
}
pub fn resolve_mask_source_arg(
requested: MaskSourceArg,
processing_requested: bool,
) -> MaskSourceArg {
match requested {
MaskSourceArg::Auto => {
if processing_requested {
MaskSourceArg::Processed
} else {
MaskSourceArg::Raw
}
}
other => other,
}
}
pub fn resolve_mask_export_source(
requested: MaskExportSource,
processing_requested: bool,
) -> MaskExportSource {
match requested {
MaskExportSource::Auto => {
if processing_requested {
MaskExportSource::Processed
} else {
MaskExportSource::Raw
}
}
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
mod resolve_model_path {
use super::*;
use crate::cli::ResampleFilter;
use std::fs;
use tempfile::TempDir;
fn temp_dir(prefix: &str) -> TempDir {
tempfile::Builder::new()
.prefix(prefix)
.tempdir()
.expect("failed to create temp dir")
}
fn make_global(model: Option<PathBuf>) -> GlobalOptions {
GlobalOptions {
model,
intra_threads: None,
model_input_size: None,
input_resample_filter: ResampleFilter::Triangle,
output_resample_filter: ResampleFilter::Lanczos3,
}
}
#[test]
fn explicit_model_is_used_as_is() {
let global = make_global(Some(PathBuf::from("explicit.onnx")));
assert_eq!(
super::resolve_model_path_impl(&global, None),
PathBuf::from("explicit.onnx")
);
}
#[cfg(feature = "fetch-model")]
#[test]
fn cached_model_used_when_present() {
let cache_dir = temp_dir("outline-model-resolve-cache");
let cached = cache_dir.path().join("model.onnx");
fs::write(&cached, b"cached").expect("failed to write cached model");
let global = make_global(None);
assert_eq!(
super::resolve_model_path_impl(&global, Some(&cached)),
cached
);
}
#[test]
fn default_returned_when_nothing_available() {
let global = make_global(None);
assert_eq!(
super::resolve_model_path_impl(&global, None),
PathBuf::from("model.onnx")
);
}
#[cfg(feature = "fetch-model")]
#[test]
fn default_returned_when_cached_path_missing() {
let cache_dir = temp_dir("outline-model-resolve-missing-cache");
let missing_cached = cache_dir.path().join("model.onnx");
let global = make_global(None);
assert_eq!(
super::resolve_model_path_impl(&global, Some(&missing_cached)),
PathBuf::from("model.onnx")
);
}
#[test]
fn explicit_model_preferred_over_cache() {
let cache_dir = temp_dir("outline-model-resolve-prefer-explicit");
let cached = cache_dir.path().join("model.onnx");
fs::write(&cached, b"cached").expect("failed to write cached model");
let global = make_global(Some(PathBuf::from("my-model.onnx")));
assert_eq!(
super::resolve_model_path_impl(&global, Some(&cached)),
PathBuf::from("my-model.onnx")
);
}
#[cfg(feature = "fetch-model")]
#[test]
fn cached_preferred_over_default() {
let cache_dir = temp_dir("outline-model-resolve-cached-over-default");
let cached = cache_dir.path().join("model.onnx");
fs::write(&cached, b"cached").expect("failed to write cached model");
let global = make_global(None);
let result = super::resolve_model_path_impl(&global, Some(&cached));
assert_eq!(result, cached);
assert_ne!(result, PathBuf::from(outline::DEFAULT_MODEL_PATH));
}
}
mod derive_variant_path {
use super::*;
#[test]
fn basic() {
let input = Path::new("/path/to/image.png");
let result = derive_variant_path(input, "matte", "png");
assert_eq!(result, PathBuf::from("/path/to/image-matte.png"));
}
#[test]
fn different_extension() {
let input = Path::new("/path/to/photo.jpg");
let result = derive_variant_path(input, "mask", "png");
assert_eq!(result, PathBuf::from("/path/to/photo-mask.png"));
}
#[test]
fn no_extension() {
let input = Path::new("/path/to/image");
let result = derive_variant_path(input, "foreground", "png");
assert_eq!(result, PathBuf::from("/path/to/image-foreground.png"));
}
#[test]
fn relative_path() {
let input = Path::new("image.png");
let result = derive_variant_path(input, "composite", "png");
assert_eq!(result, PathBuf::from("image-composite.png"));
}
#[test]
fn multi_dot_name() {
let input = Path::new("/path/to/archive.tar.gz");
let result = derive_variant_path(input, "mask", "png");
assert_eq!(result, PathBuf::from("/path/to/archive.tar-mask.png"));
}
}
mod resolve_export_path {
use super::*;
#[test]
fn none_returns_none() {
let opt: Option<Option<PathBuf>> = None;
let input = Path::new("/path/to/image.png");
let result = resolve_export_path(&opt, input, "matte");
assert_eq!(result, None);
}
#[test]
fn some_none_uses_default() {
let opt: Option<Option<PathBuf>> = Some(None);
let input = Path::new("/path/to/image.png");
let result = resolve_export_path(&opt, input, "matte");
assert_eq!(result, Some(PathBuf::from("/path/to/image-matte.png")));
}
#[test]
fn some_some_uses_custom() {
let custom_path = PathBuf::from("/custom/output.png");
let opt: Option<Option<PathBuf>> = Some(Some(custom_path.clone()));
let input = Path::new("/path/to/image.png");
let result = resolve_export_path(&opt, input, "matte");
assert_eq!(result, Some(custom_path));
}
#[test]
fn different_suffixes() {
let opt: Option<Option<PathBuf>> = Some(None);
let input = Path::new("photo.jpg");
assert_eq!(
resolve_export_path(&opt, input, "foreground"),
Some(PathBuf::from("photo-foreground.png"))
);
assert_eq!(
resolve_export_path(&opt, input, "mask"),
Some(PathBuf::from("photo-mask.png"))
);
assert_eq!(
resolve_export_path(&opt, input, "bg-layer"),
Some(PathBuf::from("photo-bg-layer.png"))
);
}
}
mod derive_svg_path {
use super::*;
#[test]
fn changes_extension() {
let input = Path::new("/path/to/image.png");
let result = derive_svg_path(input);
assert_eq!(result, PathBuf::from("/path/to/image.svg"));
}
#[test]
fn no_extension() {
let input = Path::new("/path/to/image");
let result = derive_svg_path(input);
assert_eq!(result, PathBuf::from("/path/to/image.svg"));
}
}
mod resolve_alpha_source {
use super::*;
#[test]
fn auto_with_processing() {
let result = resolve_alpha_source(AlphaFromArg::Auto, true);
assert!(matches!(result, AlphaFromArg::Processed));
}
#[test]
fn auto_without_processing() {
let result = resolve_alpha_source(AlphaFromArg::Auto, false);
assert!(matches!(result, AlphaFromArg::Raw));
}
#[test]
fn explicit_raw() {
let result = resolve_alpha_source(AlphaFromArg::Raw, true);
assert!(matches!(result, AlphaFromArg::Raw));
}
#[test]
fn explicit_processed() {
let result = resolve_alpha_source(AlphaFromArg::Processed, false);
assert!(matches!(result, AlphaFromArg::Processed));
}
}
mod processing_requested {
use crate::cli::{CliMaskProcessingStep, MaskProcessingArgs};
fn default_args() -> MaskProcessingArgs {
MaskProcessingArgs {
blur: None,
threshold: None,
no_implicit_threshold: false,
dilate: None,
erode: None,
erode_border: None,
fill_holes: None,
ordered_steps: vec![],
}
}
#[test]
fn false_for_defaults() {
assert!(!super::processing_requested(&default_args()));
}
#[test]
fn true_for_threshold_only() {
let args = MaskProcessingArgs {
ordered_steps: vec![CliMaskProcessingStep::Threshold(200)],
..default_args()
};
assert!(super::processing_requested(&args));
}
#[test]
fn true_for_default_threshold() {
let args = MaskProcessingArgs {
ordered_steps: vec![CliMaskProcessingStep::Threshold(120)],
..default_args()
};
assert!(super::processing_requested(&args));
}
}
mod resolve_mask_source_arg {
use super::*;
#[test]
fn auto_with_processing() {
let result = resolve_mask_source_arg(MaskSourceArg::Auto, true);
assert!(matches!(result, MaskSourceArg::Processed));
}
#[test]
fn auto_without_processing() {
let result = resolve_mask_source_arg(MaskSourceArg::Auto, false);
assert!(matches!(result, MaskSourceArg::Raw));
}
#[test]
fn explicit_raw() {
let result = resolve_mask_source_arg(MaskSourceArg::Raw, true);
assert!(matches!(result, MaskSourceArg::Raw));
}
#[test]
fn explicit_processed() {
let result = resolve_mask_source_arg(MaskSourceArg::Processed, false);
assert!(matches!(result, MaskSourceArg::Processed));
}
}
mod resolve_mask_export_source {
use super::*;
#[test]
fn auto_with_processing() {
let result = resolve_mask_export_source(MaskExportSource::Auto, true);
assert!(matches!(result, MaskExportSource::Processed));
}
#[test]
fn auto_without_processing() {
let result = resolve_mask_export_source(MaskExportSource::Auto, false);
assert!(matches!(result, MaskExportSource::Raw));
}
#[test]
fn explicit_raw() {
let result = resolve_mask_export_source(MaskExportSource::Raw, true);
assert!(matches!(result, MaskExportSource::Raw));
}
#[test]
fn explicit_processed() {
let result = resolve_mask_export_source(MaskExportSource::Processed, false);
assert!(matches!(result, MaskExportSource::Processed));
}
}
}