rawbit 0.1.16

A camera RAW image preprocessor and importer
use std::{
    error,
    fs::{create_dir_all, remove_file},
    path::{Path, PathBuf},
};

use tokio::{
    fs::OpenOptions,
    io::{self, AsyncReadExt as _},
};

use async_trait::async_trait;
use rawler::{
    RawlerError,
    decoders::{RawDecodeParams, RawMetadata},
    dng::{self, convert::ConvertParams},
    get_decoder,
    rawsource::RawSource,
};

use smlog::info;

use crate::{common::map_err, parse::FilenameFormat};

#[derive(Debug)]
pub enum Error {
    ImgOp(String, RawlerError),
    Io(String, io::Error),
    AlreadyExists(String),
    #[allow(unused)]
    Other(String, Box<dyn error::Error + Send + Sync>),
}

#[async_trait]
pub trait Job {
    fn new(config: JobConfig) -> Self;
    async fn run(self) -> Result<(), Error>;
}

#[derive(Debug)]
pub struct JobConfig {
    pub input_path: PathBuf,
    pub output_dir: PathBuf,
    pub filename_format: &'static FilenameFormat<'static>,
    pub force: bool,
    pub convert_opts: ConvertParams,
}

#[derive(Debug)]
pub struct RawConvertJob(JobConfig);

fn build_output_filename(input_path: &Path, fmt: &FilenameFormat, md: &RawMetadata) -> PathBuf {
    let input_fname_no_ext = input_path
        .file_stem()
        .unwrap_or_else(|| panic!("couldn't deduce filename from {}", input_path.display()))
        .to_string_lossy();

    let output_fname = fmt.render_filename(input_fname_no_ext.as_ref(), md) + ".dng";

    output_fname.into()
}

impl RawConvertJob {
    async fn run_async(self) -> Result<(), Error> {
        let config = self.0;

        let mut input = map_err!(
            OpenOptions::new()
                .read(true)
                .write(false)
                .open(&config.input_path)
                .await,
            Error::Io,
            "Couldn't open input RAW file",
        )?;

        let mut buf = vec![];

        map_err!(
            input.read_to_end(&mut buf).await,
            Error::Io,
            format!("couldn't read from file: '{}'", config.input_path.display())
        )?;

        let raw_file = RawSource::new_from_slice(&buf[..]);

        let decoder = map_err!(
            get_decoder(&raw_file),
            Error::ImgOp,
            "no compatible RAW image decoder available",
        )?;

        let md = map_err!(
            decoder.raw_metadata(&raw_file, &RawDecodeParams::default()),
            Error::ImgOp,
            "couldn't extract image metadata",
        )?;

        let transformed_fname =
            build_output_filename(&config.input_path, config.filename_format, &md);

        map_err!(
            create_dir_all(&config.output_dir),
            Error::Io,
            format!("couldn't make output dir: {}", config.output_dir.display())
        )?;

        let output_path = config.output_dir.join(transformed_fname);

        if output_path.exists() {
            if !config.force {
                Err(Error::AlreadyExists(format!(
                    "won't overwrite existing file: {}",
                    output_path.display()
                )))
            } else if output_path.is_dir() {
                Err(Error::AlreadyExists(format!(
                    "computed filepath already exists as a directory: {}",
                    output_path.display()
                )))
            } else {
                map_err!(
                    remove_file(&output_path),
                    Error::Io,
                    format!("couldn't remove existing file: {}", output_path.display()),
                )
            }?;
        }

        let output_file = std::fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&output_path);

        map_err!(
            tokio::task::spawn_blocking(move || {
                let mut output_file = std::io::BufWriter::new(map_err!(
                    output_file,
                    Error::Io,
                    format!("couldn't create output file: {}", output_path.display()),
                )?);

                info!("Writing DNG: \"{}\"", output_path.display());

                let cvt_result = dng::convert::convert_raw_source(
                    &raw_file,
                    &mut output_file,
                    config.input_path.to_string_lossy(),
                    &config.convert_opts,
                );

                map_err!(cvt_result, Error::ImgOp, "couldn't convert image to DNG",)
            })
            .await
            .map_err(Box::new),
            Error::Other,
            format!("async error")
        )?
    }
}

#[async_trait]
impl Job for RawConvertJob {
    fn new(config: JobConfig) -> Self {
        assert!(config.input_path.is_file());

        Self(config)
    }

    async fn run(self) -> Result<(), Error> {
        self.run_async().await
    }
}

pub struct DryRunJob(JobConfig);

const DECODE_PARAMS: RawDecodeParams = RawDecodeParams { image_index: 0 };

#[async_trait]
impl Job for DryRunJob {
    fn new(config: JobConfig) -> Self {
        assert!(config.input_path.is_file());

        Self(config)
    }

    async fn run(self) -> Result<(), Error> {
        let config = self.0;

        let input_file = OpenOptions::new()
            .read(true)
            .write(false)
            .open(&config.input_path)
            .await;

        let mut input_file = map_err!(
            input_file,
            Error::Io,
            format!("couldn't read file: {}", config.input_path.display())
        )?;

        let mut buf = vec![];
        map_err!(
            input_file.read_to_end(&mut buf).await,
            Error::Io,
            format!("couldn't read from file: '{}'", config.input_path.display())
        )?;

        let src = RawSource::new_from_slice(&buf[..]).with_path(&config.input_path);

        let decoder = map_err!(get_decoder(&src), Error::ImgOp, "no available decoder")?;
        let md = map_err!(
            decoder.raw_metadata(&src, &DECODE_PARAMS),
            Error::ImgOp,
            format!(
                "error while retreiving metadata from RAW: {}",
                config.input_path.display()
            )
        )?;

        let output_fname = build_output_filename(&config.input_path, config.filename_format, &md);

        let output_path = config.output_dir.join(output_fname);

        info!("dry run: would've written DNG: {}", output_path.display());

        Ok(())
    }
}