filmborders 0.0.32

add hipster film borders to your images :)
Documentation
use clap::Parser;
#[cfg(feature = "builtin")]
use filmborders::builtin;
use filmborders::{
    border::{self, Border},
    debug::Instant,
    error::Report,
    img, types, Error, ImageBorders,
};
use std::path::PathBuf;
#[cfg(feature = "builtin")]
use std::str::FromStr;

#[derive(Parser, Debug, Clone)]
#[clap(
    name = "film-borders",
    version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
    about = "add film borders to an image",
    author = "romnn <contact@romnn.com>",
)]
struct Options {
    #[clap(short = 'i', long = "image")]
    images: Vec<PathBuf>,

    #[clap(short = 'o', long = "output")]
    output: Option<PathBuf>,

    #[clap(short = 'b', long = "border")]
    border: Option<String>,

    #[clap(long = "width")]
    output_width: Option<u32>,

    #[clap(long = "height")]
    output_height: Option<u32>,

    #[clap(long = "max-width")]
    max_output_width: Option<u32>,

    #[clap(long = "max-height")]
    max_output_height: Option<u32>,

    #[clap(long = "margin", aliases = &["margin-factor"])]
    margin: Option<f32>,

    #[clap(long = "scale", aliases = &["scale-factor"])]
    scale_factor: Option<f32>,

    #[clap(long = "crop-top")]
    crop_top: Option<f32>,
    #[clap(long = "crop-right")]
    crop_right: Option<f32>,
    #[clap(long = "crop-bottom")]
    crop_bottom: Option<f32>,
    #[clap(long = "crop-left")]
    crop_left: Option<f32>,

    #[clap(long = "frame-width")]
    frame_width: Option<f32>,

    #[clap(long = "fit", help = "fitting mode")]
    mode: Option<types::FitMode>,

    #[clap(long = "rotate", aliases = &["rotate-image"])]
    image_rotation: Option<types::Rotation>,

    #[clap(long = "rotate-border")]
    border_rotation: Option<types::Rotation>,

    #[clap(long = "background-color", help = "background color in HEX format")]
    background_color: Option<types::Color>,

    #[clap(long = "frame-color", help = "frame color in HEX format")]
    frame_color: Option<types::Color>,

    #[clap(long = "preview", help = "overlay instagram preview visiable area", action = clap::ArgAction::SetTrue)]
    preview: bool,

    #[clap(long = "no-border", action = clap::ArgAction::SetTrue)]
    no_border: bool,

    #[clap(long = "quality", help = "output image quality (1-100)")]
    quality: Option<u8>,

    #[clap(short = 'v', parse(from_occurrences))]
    verbosity: u8,
}

fn main() {
    let options = Options::parse();
    let start = Instant::now();
    let images = options
        .images
        .iter()
        .map(|image_path| img::Image::open(image_path).map_err(Error::from))
        .collect::<Result<Vec<img::Image>, Error>>();

    match images.and_then(ImageBorders::new) {
        Ok(mut borders) => {
            let border = if options.no_border {
                None
            } else {
                #[cfg(feature = "builtin")]
                let border = match options.border {
                    None => Ok(border::Kind::default()),
                    Some(border) => builtin::Builtin::from_str(&border)
                        .map(border::Kind::Builtin)
                        .or_else(|_| {
                            Border::open(PathBuf::from(border), None).map(border::Kind::Custom)
                        }),
                };

                #[cfg(not(feature = "builtin"))]
                let border = options
                    .border
                    .ok_or(border::Error::Missing)
                    .and_then(|border| {
                        Border::open(PathBuf::from(border), None)
                            .map(border::Kind::Custom)
                            .map_err(border::Error::from)
                    });

                let border = match border {
                    Ok(border) => border,
                    Err(err) => {
                        eprintln!("failed to read border: {}", err.report());
                        return;
                    }
                };
                Some(border)
            };

            let border_options = filmborders::Options {
                output_size: types::BoundedSize {
                    width: options.output_width,
                    height: options.output_height,
                },
                output_size_bounds: types::BoundedSize {
                    width: options.max_output_width,
                    height: options.max_output_height,
                },
                mode: options.mode.unwrap_or_default(),
                crop: Some(types::sides::percent::Sides {
                    top: options.crop_top.unwrap_or(0.0),
                    right: options.crop_right.unwrap_or(0.0),
                    bottom: options.crop_bottom.unwrap_or(0.0),
                    left: options.crop_left.unwrap_or(0.0),
                }),
                scale_factor: options.scale_factor.unwrap_or(1.0),
                margin: options.margin.unwrap_or(0.05),
                frame_width: types::sides::percent::Sides::uniform(
                    options.frame_width.unwrap_or(0.01),
                ),
                image_rotation: options.image_rotation.unwrap_or_default(),
                border_rotation: options.border_rotation.unwrap_or_default(),
                background_color: options.background_color,
                frame_color: options.frame_color.unwrap_or_else(types::Color::black),

                preview: options.preview,
            };
            filmborders::debug!(&border_options);
            match borders
                .render(border, &border_options)
                .map_err(Error::from)
                .and_then(|result| match options.output {
                    Some(output) => result
                        .save_with_filename(output, options.quality)
                        .map_err(img::Error::from)
                        .map_err(Error::from),
                    None => result
                        .save(options.quality)
                        .map_err(img::Error::from)
                        .map_err(Error::from),
                }) {
                Ok(_) => {
                    println!("completed in {} msec", start.elapsed_millis());
                }
                Err(err) => eprintln!("{}", err.report()),
            };
        }
        Err(err) => eprintln!("{}", err.report()),
    }
}