use pixelflow_core::{
ClipMedia, ErrorCategory, ErrorCode, FilterOptionValue, FilterOptions, FilterPlanRequest,
PixelFlowError, Result,
};
use crate::DitherMode;
pub(crate) fn single_input_media<'a>(
request: FilterPlanRequest<'a>,
filter_name: &str,
) -> Result<&'a ClipMedia> {
single_input_slice(request.input_media(), filter_name)
}
pub(crate) fn single_input_slice<'a>(
input_media: &'a [ClipMedia],
filter_name: &str,
) -> Result<&'a ClipMedia> {
match input_media {
[input] => Ok(input),
inputs => Err(PixelFlowError::new(
ErrorCategory::Graph,
ErrorCode::new("graph.invalid_filter_inputs"),
format!(
"filter '{filter_name}' requires exactly one input clip, got {}",
inputs.len()
),
)),
}
}
pub(crate) fn optional_string<'a>(
options: &'a FilterOptions,
filter_name: &str,
name: &str,
code: &'static str,
) -> Result<Option<&'a str>> {
match options.get(name) {
None | Some(FilterOptionValue::None) => Ok(None),
Some(FilterOptionValue::String(value)) => Ok(Some(value.as_str())),
Some(_) => Err(filter_option_error(
code,
format!("filter '{filter_name}' option '{name}' must be string"),
)),
}
}
pub(crate) fn required_string<'a>(
options: &'a FilterOptions,
filter_name: &str,
name: &str,
code: &'static str,
) -> Result<&'a str> {
optional_string(options, filter_name, name, code)?.ok_or_else(|| {
filter_option_error(
code,
format!("filter '{filter_name}' requires option '{name}'"),
)
})
}
pub(crate) fn optional_i64(
options: &FilterOptions,
filter_name: &str,
name: &str,
code: &'static str,
) -> Result<Option<i64>> {
match options.get(name) {
None | Some(FilterOptionValue::None) => Ok(None),
Some(FilterOptionValue::Int(value)) => Ok(Some(*value)),
Some(_) => Err(filter_option_error(
code,
format!("filter '{filter_name}' option '{name}' must be integer"),
)),
}
}
pub(crate) fn required_i64(
options: &FilterOptions,
filter_name: &str,
name: &str,
code: &'static str,
) -> Result<i64> {
optional_i64(options, filter_name, name, code)?.ok_or_else(|| {
filter_option_error(
code,
format!("filter '{filter_name}' requires option '{name}'"),
)
})
}
pub(crate) fn optional_f64(
options: &FilterOptions,
filter_name: &str,
name: &str,
code: &'static str,
) -> Result<Option<f64>> {
match options.get(name) {
None | Some(FilterOptionValue::None) => Ok(None),
Some(FilterOptionValue::Int(value)) => Ok(Some(*value as f64)),
Some(FilterOptionValue::Float(value)) => Ok(Some(*value)),
Some(_) => Err(filter_option_error(
code,
format!("filter '{filter_name}' option '{name}' must be number"),
)),
}
}
pub(crate) fn optional_dither(
options: &FilterOptions,
filter_name: &str,
code: &'static str,
) -> Result<Option<DitherMode>> {
let Some(dither) = optional_string(options, filter_name, "dither", code)? else {
return Ok(None);
};
match dither.to_ascii_lowercase().as_str() {
"fruit" => Ok(Some(DitherMode::Fruit)),
"none" => Ok(Some(DitherMode::None)),
_ => Err(filter_option_error(
code,
format!("filter '{filter_name}' option 'dither' must be 'fruit' or 'none'"),
)),
}
}
pub(crate) fn filter_option_error(
code: &'static str,
message: impl Into<String>,
) -> PixelFlowError {
PixelFlowError::new(ErrorCategory::Format, ErrorCode::new(code), message)
}
#[cfg(test)]
mod tests {
use pixelflow_core::{ErrorCategory, ErrorCode};
#[test]
fn single_input_slice_reuses_existing_input_diagnostic() {
let inputs = [
crate::testkit::fixed_media("gray8", 2, 2),
crate::testkit::fixed_media("gray8", 2, 2),
];
let error = super::single_input_slice(&inputs, "resize")
.expect_err("two inputs should fail for single-input filters");
assert_eq!(error.category(), ErrorCategory::Graph);
assert_eq!(error.code(), ErrorCode::new("graph.invalid_filter_inputs"));
assert!(error.message().contains("resize"));
assert!(error.message().contains("2"));
}
}