panlabel 0.7.0

The universal annotation converter
Documentation
use std::path::{Component, Path};

use serde_json::Value;

use super::{BBoxXYXY, Pixel};
use crate::error::PanlabelError;

pub(super) fn has_json_extension(path: &Path) -> bool {
    path.extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
}

pub(super) fn scalar_to_string(value: Option<&Value>) -> Option<String> {
    match value? {
        Value::String(value) => Some(value.clone()),
        Value::Number(value) => Some(value.to_string()),
        Value::Bool(value) => Some(value.to_string()),
        _ => None,
    }
}

pub(super) fn required_u32(
    value: Option<&Value>,
    path: &Path,
    field: &str,
    invalid: fn(&Path, String) -> PanlabelError,
) -> Result<u32, PanlabelError> {
    let number = value.and_then(Value::as_u64).ok_or_else(|| {
        invalid(
            path,
            format!("missing or invalid unsigned integer field '{field}'"),
        )
    })?;
    u32::try_from(number)
        .map_err(|_| invalid(path, format!("field '{field}' is too large for u32")))
}

pub(super) fn required_f64(
    value: Option<&Value>,
    path: &Path,
    field: impl AsRef<str>,
    invalid: fn(&Path, String) -> PanlabelError,
) -> Result<f64, PanlabelError> {
    let field = field.as_ref();
    let number = value
        .and_then(Value::as_f64)
        .ok_or_else(|| invalid(path, format!("missing or invalid number field '{field}'")))?;
    if number.is_finite() {
        Ok(number)
    } else {
        Err(invalid(path, format!("field '{field}' must be finite")))
    }
}

pub(super) fn optional_finite_f64(
    value: Option<&Value>,
    path: &Path,
    field: impl AsRef<str>,
    invalid: fn(&Path, String) -> PanlabelError,
) -> Result<Option<f64>, PanlabelError> {
    let Some(value) = value else {
        return Ok(None);
    };
    let number = value
        .as_f64()
        .ok_or_else(|| invalid(path, format!("field '{}' must be a number", field.as_ref())))?;
    if number.is_finite() {
        Ok(Some(number))
    } else {
        Err(invalid(
            path,
            format!("field '{}' must be finite", field.as_ref()),
        ))
    }
}

pub(super) fn parse_point_pair(
    point: &Value,
    path: &Path,
    field: String,
    invalid: fn(&Path, String) -> PanlabelError,
) -> Result<[f64; 2], PanlabelError> {
    let array = point
        .as_array()
        .ok_or_else(|| invalid(path, format!("{field} must be a [x, y] array")))?;
    if array.len() != 2 {
        return Err(invalid(
            path,
            format!("{field} must have exactly 2 numbers"),
        ));
    }
    Ok([
        required_f64(array.first(), path, format!("{field}[0]"), invalid)?,
        required_f64(array.get(1), path, format!("{field}[1]"), invalid)?,
    ])
}

pub(super) fn envelope(points: &[[f64; 2]]) -> BBoxXYXY<Pixel> {
    let mut xmin = f64::INFINITY;
    let mut ymin = f64::INFINITY;
    let mut xmax = f64::NEG_INFINITY;
    let mut ymax = f64::NEG_INFINITY;
    for [x, y] in points {
        xmin = xmin.min(*x);
        ymin = ymin.min(*y);
        xmax = xmax.max(*x);
        ymax = ymax.max(*y);
    }
    BBoxXYXY::<Pixel>::from_xyxy(xmin, ymin, xmax, ymax)
}

pub(super) fn reject_unsafe_relative_path(
    file_name: &str,
    path: &Path,
    invalid: fn(&Path, String) -> PanlabelError,
) -> Result<(), PanlabelError> {
    let candidate = Path::new(file_name);
    if file_name.trim().is_empty() {
        return Err(invalid(
            path,
            "image file_name must not be empty".to_string(),
        ));
    }
    for component in candidate.components() {
        match component {
            Component::Normal(_) => {}
            Component::CurDir
            | Component::ParentDir
            | Component::RootDir
            | Component::Prefix(_) => {
                return Err(invalid(
                    path,
                    format!(
                        "unsafe image file_name '{file_name}' cannot be used as an output annotation path"
                    ),
                ));
            }
        }
    }
    Ok(())
}