use crate::{milo, render_map as render_map_into, MiloError, MiloOptions, MiloOutcome, Rgba8};
use blazediff_shared::Image;
use napi::bindgen_prelude::*;
use napi_derive::napi;
#[napi(object)]
pub struct NapiMiloOptions {
pub max_error: Option<f64>,
pub threads: Option<u32>,
pub return_maps: Option<bool>,
pub compression: Option<u8>,
pub quality: Option<u8>,
}
#[napi(object)]
pub struct NapiMiloResult {
pub match_result: bool,
pub reason: Option<String>,
pub raw_error: f64,
pub mos: f64,
pub mask: Option<Float32Array>,
pub error_map: Option<Float32Array>,
pub width: u32,
pub height: u32,
}
impl From<MiloError> for Error {
fn from(error: MiloError) -> Self {
Error::new(Status::InvalidArg, error.to_string())
}
}
fn milo_options(options: &Option<NapiMiloOptions>) -> MiloOptions {
MiloOptions {
threads: options
.as_ref()
.and_then(|o| o.threads)
.map(|threads| threads as usize),
}
}
fn view(image: &Image) -> Rgba8<'_> {
Rgba8::new(&image.data, image.width as usize, image.height as usize)
}
fn shape(outcome: MiloOutcome, options: &Option<NapiMiloOptions>) -> NapiMiloResult {
let max_error = options.as_ref().and_then(|o| o.max_error).unwrap_or(0.0);
let match_result = outcome.raw_error <= max_error;
let return_maps = options
.as_ref()
.and_then(|o| o.return_maps)
.unwrap_or(false);
NapiMiloResult {
match_result,
reason: if match_result {
None
} else {
Some("error-above-threshold".to_string())
},
raw_error: outcome.raw_error,
mos: outcome.mos,
width: outcome.width as u32,
height: outcome.height as u32,
mask: return_maps.then(|| Float32Array::new(outcome.mask)),
error_map: return_maps.then(|| Float32Array::new(outcome.error_map)),
}
}
fn write_map(path: &str, outcome: &MiloOutcome, options: &Option<NapiMiloOptions>) -> Result<()> {
let mut image = Image::new(outcome.width as u32, outcome.height as u32);
render_map_into(
&mut image.data,
outcome.width,
outcome.height,
&outcome.error_map,
);
let compression = options.as_ref().and_then(|o| o.compression).unwrap_or(0);
let quality = options.as_ref().and_then(|o| o.quality).unwrap_or(90);
blazediff_shared::save_image(&image, path, compression, quality)
.map_err(|e| Error::new(Status::GenericFailure, format!("Failed to save map: {e}")))
}
fn compare_images(
image1: Image,
image2: Image,
map_output: Option<String>,
options: Option<NapiMiloOptions>,
) -> Result<NapiMiloResult> {
let outcome = milo(view(&image1), view(&image2), &milo_options(&options))?;
if let Some(path) = &map_output {
write_map(path, &outcome, &options)?;
}
Ok(shape(outcome, &options))
}
#[napi]
pub fn compare(
base_path: String,
compare_path: String,
map_output: Option<String>,
options: Option<NapiMiloOptions>,
) -> Result<NapiMiloResult> {
let (image1, image2) =
blazediff_shared::load_image_pair(&base_path, &compare_path).map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to load images: {e}"),
)
})?;
compare_images(image1, image2, map_output, options)
}
#[napi]
pub fn compare_buffers(
base: &[u8],
comparison: &[u8],
map_output: Option<String>,
options: Option<NapiMiloOptions>,
) -> Result<NapiMiloResult> {
let (image1, image2) = blazediff_shared::decode_image_pair(base, comparison).map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to load images: {e}"),
)
})?;
compare_images(image1, image2, map_output, options)
}
#[napi]
pub fn compare_rgba(
base: &[u8],
comparison: &[u8],
width: u32,
height: u32,
options: Option<NapiMiloOptions>,
) -> Result<NapiMiloResult> {
let (width, height) = (width as usize, height as usize);
let outcome = milo(
Rgba8::new(base, width, height),
Rgba8::new(comparison, width, height),
&milo_options(&options),
)?;
Ok(shape(outcome, &options))
}
#[napi]
pub fn render_map(map: Float32Array, width: u32, height: u32) -> Result<Buffer> {
let (width, height) = (width as usize, height as usize);
let mut output = vec![0u8; width * height * 4];
render_map_into(&mut output, width, height, map.as_ref());
Ok(output.into())
}