use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use web_sys::ImageData;
use crate::error::{CanvasError, WasmError, WasmResult};
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub const fn from_gray(value: u8) -> Self {
Self::new(value, value, value)
}
pub fn to_gray(&self) -> u8 {
((77 * u16::from(self.r) + 150 * u16::from(self.g) + 29 * u16::from(self.b)) / 256) as u8
}
pub fn to_hsv(&self) -> Hsv {
let r = f64::from(self.r) / 255.0;
let g = f64::from(self.g) / 255.0;
let b = f64::from(self.b) / 255.0;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let h = if delta < f64::EPSILON {
0.0
} else if (max - r).abs() < f64::EPSILON {
60.0 * (((g - b) / delta) % 6.0)
} else if (max - g).abs() < f64::EPSILON {
60.0 * (((b - r) / delta) + 2.0)
} else {
60.0 * (((r - g) / delta) + 4.0)
};
let s = if max < f64::EPSILON { 0.0 } else { delta / max };
let v = max;
Hsv {
h: if h < 0.0 { h + 360.0 } else { h },
s,
v,
}
}
pub fn to_ycbcr(&self) -> YCbCr {
let r = f64::from(self.r);
let g = f64::from(self.g);
let b = f64::from(self.b);
let y = 0.299 * r + 0.587 * g + 0.114 * b;
let cb = 128.0 + (-0.168736 * r - 0.331264 * g + 0.5 * b);
let cr = 128.0 + (0.5 * r - 0.418688 * g - 0.081312 * b);
YCbCr {
y: y.clamp(0.0, 255.0) as u8,
cb: cb.clamp(0.0, 255.0) as u8,
cr: cr.clamp(0.0, 255.0) as u8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hsv {
pub h: f64,
pub s: f64,
pub v: f64,
}
impl Hsv {
pub const fn new(h: f64, s: f64, v: f64) -> Self {
Self { h, s, v }
}
pub fn to_rgb(&self) -> Rgb {
let c = self.v * self.s;
let h_prime = self.h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = self.v - c;
let (r, g, b) = if h_prime < 1.0 {
(c, x, 0.0)
} else if h_prime < 2.0 {
(x, c, 0.0)
} else if h_prime < 3.0 {
(0.0, c, x)
} else if h_prime < 4.0 {
(0.0, x, c)
} else if h_prime < 5.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
Rgb::new(
((r + m) * 255.0).round() as u8,
((g + m) * 255.0).round() as u8,
((b + m) * 255.0).round() as u8,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct YCbCr {
pub y: u8,
pub cb: u8,
pub cr: u8,
}
impl YCbCr {
pub const fn new(y: u8, cb: u8, cr: u8) -> Self {
Self { y, cb, cr }
}
pub fn to_rgb(&self) -> Rgb {
let y = f64::from(self.y);
let cb = f64::from(self.cb) - 128.0;
let cr = f64::from(self.cr) - 128.0;
let r = y + 1.402 * cr;
let g = y - 0.344136 * cb - 0.714136 * cr;
let b = y + 1.772 * cb;
Rgb::new(
r.clamp(0.0, 255.0) as u8,
g.clamp(0.0, 255.0) as u8,
b.clamp(0.0, 255.0) as u8,
)
}
}
#[derive(Debug, Clone)]
pub struct Histogram {
pub red: [u32; 256],
pub green: [u32; 256],
pub blue: [u32; 256],
pub luminance: [u32; 256],
}
impl Histogram {
pub const fn new() -> Self {
Self {
red: [0; 256],
green: [0; 256],
blue: [0; 256],
luminance: [0; 256],
}
}
pub fn from_rgba(data: &[u8], width: u32, height: u32) -> WasmResult<Self> {
if width == 0 || height == 0 || data.is_empty() {
return Err(WasmError::Canvas(CanvasError::InvalidParameter(
"Width, height, and data must be non-empty".to_string(),
)));
}
let expected_len = (width as usize) * (height as usize) * 4;
if data.len() != expected_len {
return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
expected: expected_len,
actual: data.len(),
}));
}
let mut hist = Self::new();
for chunk in data.chunks_exact(4) {
let r = chunk[0];
let g = chunk[1];
let b = chunk[2];
hist.red[r as usize] += 1;
hist.green[g as usize] += 1;
hist.blue[b as usize] += 1;
let lum = Rgb::new(r, g, b).to_gray();
hist.luminance[lum as usize] += 1;
}
Ok(hist)
}
pub fn min_value(&self) -> u8 {
for (i, &count) in self.luminance.iter().enumerate() {
if count > 0 {
return i as u8;
}
}
0
}
pub fn max_value(&self) -> u8 {
for (i, &count) in self.luminance.iter().enumerate().rev() {
if count > 0 {
return i as u8;
}
}
255
}
pub fn mean(&self) -> f64 {
let total: u64 = self.luminance.iter().map(|&x| u64::from(x)).sum();
if total == 0 {
return 0.0;
}
let weighted_sum: u64 = self
.luminance
.iter()
.enumerate()
.map(|(val, &count)| val as u64 * u64::from(count))
.sum();
weighted_sum as f64 / total as f64
}
pub fn median(&self) -> u8 {
let total: u64 = self.luminance.iter().map(|&x| u64::from(x)).sum();
if total == 0 {
return 0;
}
let target = total / 2;
let mut cumulative = 0u64;
for (i, &count) in self.luminance.iter().enumerate() {
cumulative += u64::from(count);
if cumulative >= target {
return i as u8;
}
}
255
}
}
impl Default for Histogram {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelHistogramJson {
pub bins: Vec<u32>,
pub min: u8,
pub max: u8,
pub mean: f64,
pub median: u8,
pub std_dev: f64,
pub count: u64,
}
impl ChannelHistogramJson {
fn from_histogram_array(hist: &[u32; 256]) -> Self {
let count: u64 = hist.iter().map(|&x| u64::from(x)).sum();
let min = hist
.iter()
.enumerate()
.find(|&(_, &c)| c > 0)
.map(|(i, _)| i as u8)
.unwrap_or(0);
let max = hist
.iter()
.enumerate()
.rev()
.find(|&(_, &c)| c > 0)
.map(|(i, _)| i as u8)
.unwrap_or(255);
let mean = if count > 0 {
let weighted_sum: u64 = hist
.iter()
.enumerate()
.map(|(val, &c)| val as u64 * u64::from(c))
.sum();
weighted_sum as f64 / count as f64
} else {
0.0
};
let median = if count > 0 {
let target = count / 2;
let mut cumulative = 0u64;
let mut median_val = 0u8;
for (i, &c) in hist.iter().enumerate() {
cumulative += u64::from(c);
if cumulative >= target {
median_val = i as u8;
break;
}
}
median_val
} else {
0
};
let std_dev = if count > 0 {
let variance: f64 = hist
.iter()
.enumerate()
.map(|(val, &c)| {
let diff = val as f64 - mean;
diff * diff * f64::from(c)
})
.sum::<f64>()
/ count as f64;
variance.sqrt()
} else {
0.0
};
Self {
bins: hist.to_vec(),
min,
max,
mean,
median,
std_dev,
count,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomBinHistogramJson {
pub bins: Vec<u32>,
pub bin_edges: Vec<f64>,
pub min: f64,
pub max: f64,
pub num_bins: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistogramJson {
pub width: u32,
pub height: u32,
pub total_pixels: u64,
pub red: ChannelHistogramJson,
pub green: ChannelHistogramJson,
pub blue: ChannelHistogramJson,
pub luminance: ChannelHistogramJson,
}
impl HistogramJson {
pub fn from_histogram(hist: &Histogram, width: u32, height: u32) -> Self {
Self {
width,
height,
total_pixels: u64::from(width) * u64::from(height),
red: ChannelHistogramJson::from_histogram_array(&hist.red),
green: ChannelHistogramJson::from_histogram_array(&hist.green),
blue: ChannelHistogramJson::from_histogram_array(&hist.blue),
luminance: ChannelHistogramJson::from_histogram_array(&hist.luminance),
}
}
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
impl Histogram {
pub fn std_dev(&self) -> f64 {
let total: u64 = self.luminance.iter().map(|&x| u64::from(x)).sum();
if total == 0 {
return 0.0;
}
let mean = self.mean();
let variance: f64 = self
.luminance
.iter()
.enumerate()
.map(|(val, &count)| {
let diff = val as f64 - mean;
diff * diff * f64::from(count)
})
.sum::<f64>()
/ total as f64;
variance.sqrt()
}
pub fn to_json(&self, width: u32, height: u32) -> HistogramJson {
HistogramJson::from_histogram(self, width, height)
}
pub fn to_json_string(&self, width: u32, height: u32) -> Result<String, serde_json::Error> {
self.to_json(width, height).to_json_string()
}
pub fn from_rgba_with_bins(
data: &[u8],
width: u32,
height: u32,
bin_edges: &[f64],
) -> WasmResult<CustomBinHistogramJson> {
let expected_len = (width as usize) * (height as usize) * 4;
if data.len() != expected_len {
return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
expected: expected_len,
actual: data.len(),
}));
}
if bin_edges.len() < 2 {
return Err(WasmError::Canvas(CanvasError::InvalidParameter(
"bin_edges must have at least 2 elements".to_string(),
)));
}
let num_bins = bin_edges.len() - 1;
let mut bins = vec![0u32; num_bins];
let mut min_val = f64::MAX;
let mut max_val = f64::MIN;
for chunk in data.chunks_exact(4) {
let r = chunk[0];
let g = chunk[1];
let b = chunk[2];
let lum = Rgb::new(r, g, b).to_gray();
let lum_f = f64::from(lum);
min_val = min_val.min(lum_f);
max_val = max_val.max(lum_f);
for i in 0..num_bins {
if lum_f >= bin_edges[i] && lum_f < bin_edges[i + 1] {
bins[i] += 1;
break;
}
}
if (lum_f - bin_edges[num_bins]).abs() < f64::EPSILON {
bins[num_bins - 1] += 1;
}
}
Ok(CustomBinHistogramJson {
bins,
bin_edges: bin_edges.to_vec(),
min: if min_val == f64::MAX { 0.0 } else { min_val },
max: if max_val == f64::MIN { 255.0 } else { max_val },
num_bins,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageStats {
pub width: u32,
pub height: u32,
pub min: u8,
pub max: u8,
pub mean: f64,
pub median: u8,
pub std_dev: f64,
}
impl ImageStats {
pub fn from_rgba(data: &[u8], width: u32, height: u32) -> WasmResult<Self> {
let hist = Histogram::from_rgba(data, width, height)?;
let min = hist.min_value();
let max = hist.max_value();
let mean = hist.mean();
let median = hist.median();
let total: u64 = hist.luminance.iter().map(|&x| u64::from(x)).sum();
let variance: f64 = hist
.luminance
.iter()
.enumerate()
.map(|(val, &count)| {
let diff = val as f64 - mean;
diff * diff * f64::from(count)
})
.sum::<f64>()
/ total as f64;
let std_dev = variance.sqrt();
Ok(Self {
width,
height,
min,
max,
mean,
median,
std_dev,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContrastMethod {
LinearStretch,
HistogramEqualization,
AdaptiveHistogramEqualization,
}
pub struct ImageProcessor;
impl ImageProcessor {
pub fn enhance_contrast(
data: &mut [u8],
width: u32,
height: u32,
method: ContrastMethod,
) -> WasmResult<()> {
match method {
ContrastMethod::LinearStretch => Self::linear_stretch(data, width, height),
ContrastMethod::HistogramEqualization => {
Self::histogram_equalization(data, width, height)
}
ContrastMethod::AdaptiveHistogramEqualization => {
Self::adaptive_histogram_equalization(data, width, height)
}
}
}
fn linear_stretch(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
let hist = Histogram::from_rgba(data, width, height)?;
let min = hist.min_value();
let max = hist.max_value();
if min == max {
return Ok(());
}
let scale = 255.0 / (max - min) as f64;
for chunk in data.chunks_exact_mut(4) {
chunk[0] = ((chunk[0].saturating_sub(min)) as f64 * scale) as u8;
chunk[1] = ((chunk[1].saturating_sub(min)) as f64 * scale) as u8;
chunk[2] = ((chunk[2].saturating_sub(min)) as f64 * scale) as u8;
}
Ok(())
}
fn histogram_equalization(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
let hist = Histogram::from_rgba(data, width, height)?;
let total_pixels = (width as usize) * (height as usize);
let mut cdf = [0u32; 256];
cdf[0] = hist.luminance[0];
for i in 1..256 {
cdf[i] = cdf[i - 1] + hist.luminance[i];
}
let cdf_min = cdf.iter().find(|&&x| x > 0).copied().unwrap_or(0);
let mut lut = [0u8; 256];
for i in 0..256 {
if total_pixels > cdf_min as usize {
lut[i] = (((cdf[i] - cdf_min) as f64 / (total_pixels - cdf_min as usize) as f64)
* 255.0) as u8;
}
}
for chunk in data.chunks_exact_mut(4) {
let lum = Rgb::new(chunk[0], chunk[1], chunk[2]).to_gray();
let new_lum = lut[lum as usize];
if lum > 0 {
let scale = new_lum as f64 / lum as f64;
chunk[0] = ((chunk[0] as f64 * scale).min(255.0)) as u8;
chunk[1] = ((chunk[1] as f64 * scale).min(255.0)) as u8;
chunk[2] = ((chunk[2] as f64 * scale).min(255.0)) as u8;
}
}
Ok(())
}
fn adaptive_histogram_equalization(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
Self::histogram_equalization(data, width, height)
}
pub fn adjust_brightness(data: &mut [u8], delta: i32) {
for chunk in data.chunks_exact_mut(4) {
chunk[0] = (chunk[0] as i32 + delta).clamp(0, 255) as u8;
chunk[1] = (chunk[1] as i32 + delta).clamp(0, 255) as u8;
chunk[2] = (chunk[2] as i32 + delta).clamp(0, 255) as u8;
}
}
pub fn gamma_correction(data: &mut [u8], gamma: f64) {
let inv_gamma = 1.0 / gamma;
let mut lut = [0u8; 256];
for i in 0..256 {
lut[i] = ((i as f64 / 255.0).powf(inv_gamma) * 255.0) as u8;
}
for chunk in data.chunks_exact_mut(4) {
chunk[0] = lut[chunk[0] as usize];
chunk[1] = lut[chunk[1] as usize];
chunk[2] = lut[chunk[2] as usize];
}
}
pub fn adjust_contrast(data: &mut [u8], factor: f64) {
let factor = factor.max(0.0);
for chunk in data.chunks_exact_mut(4) {
for i in 0..3 {
let val = chunk[i] as f64;
let adjusted = ((val - 128.0) * factor + 128.0).clamp(0.0, 255.0);
chunk[i] = adjusted as u8;
}
}
}
pub fn adjust_saturation(data: &mut [u8], factor: f64) {
let factor = factor.max(0.0);
for chunk in data.chunks_exact_mut(4) {
let rgb = Rgb::new(chunk[0], chunk[1], chunk[2]);
let mut hsv = rgb.to_hsv();
hsv.s = (hsv.s * factor).clamp(0.0, 1.0);
let adjusted = hsv.to_rgb();
chunk[0] = adjusted.r;
chunk[1] = adjusted.g;
chunk[2] = adjusted.b;
}
}
pub fn to_grayscale(data: &mut [u8]) {
for chunk in data.chunks_exact_mut(4) {
let gray = Rgb::new(chunk[0], chunk[1], chunk[2]).to_gray();
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
}
}
pub fn invert(data: &mut [u8]) {
for chunk in data.chunks_exact_mut(4) {
chunk[0] = 255 - chunk[0];
chunk[1] = 255 - chunk[1];
chunk[2] = 255 - chunk[2];
}
}
pub fn convolve_3x3(
data: &[u8],
width: u32,
height: u32,
kernel: &[f32; 9],
) -> WasmResult<Vec<u8>> {
let w = width as usize;
let h = height as usize;
let mut output = vec![0u8; w * h * 4];
for y in 1..h - 1 {
for x in 1..w - 1 {
for c in 0..3 {
let mut sum = 0.0;
for ky in 0..3 {
for kx in 0..3 {
let px = x + kx - 1;
let py = y + ky - 1;
let idx = (py * w + px) * 4 + c;
sum += f32::from(data[idx]) * kernel[ky * 3 + kx];
}
}
let out_idx = (y * w + x) * 4 + c;
output[out_idx] = sum.clamp(0.0, 255.0) as u8;
}
let out_idx = (y * w + x) * 4 + 3;
let in_idx = (y * w + x) * 4 + 3;
output[out_idx] = data[in_idx];
}
}
Ok(output)
}
pub fn gaussian_blur(data: &[u8], width: u32, height: u32) -> WasmResult<Vec<u8>> {
#[allow(clippy::excessive_precision)]
let kernel = [
1.0 / 16.0,
2.0 / 16.0,
1.0 / 16.0,
2.0 / 16.0,
4.0 / 16.0,
2.0 / 16.0,
1.0 / 16.0,
2.0 / 16.0,
1.0 / 16.0,
];
Self::convolve_3x3(data, width, height, &kernel)
}
pub fn edge_detection(data: &[u8], width: u32, height: u32) -> WasmResult<Vec<u8>> {
let sobel_x = [-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
let sobel_y = [-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0];
let gx = Self::convolve_3x3(data, width, height, &sobel_x)?;
let gy = Self::convolve_3x3(data, width, height, &sobel_y)?;
let mut output = vec![0u8; gx.len()];
for i in (0..gx.len()).step_by(4) {
for c in 0..3 {
let gx_val = f64::from(gx[i + c]);
let gy_val = f64::from(gy[i + c]);
let magnitude = (gx_val * gx_val + gy_val * gy_val).sqrt();
output[i + c] = magnitude.min(255.0) as u8;
}
output[i + 3] = 255; }
Ok(output)
}
pub fn sharpen(data: &[u8], width: u32, height: u32) -> WasmResult<Vec<u8>> {
let kernel = [0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0];
Self::convolve_3x3(data, width, height, &kernel)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResampleMethod {
NearestNeighbor,
Bilinear,
Bicubic,
}
pub struct Resampler;
impl Resampler {
pub fn resample(
data: &[u8],
src_width: u32,
src_height: u32,
dst_width: u32,
dst_height: u32,
method: ResampleMethod,
) -> WasmResult<Vec<u8>> {
match method {
ResampleMethod::NearestNeighbor => {
Self::nearest_neighbor(data, src_width, src_height, dst_width, dst_height)
}
ResampleMethod::Bilinear => {
Self::bilinear(data, src_width, src_height, dst_width, dst_height)
}
ResampleMethod::Bicubic => {
Self::bicubic(data, src_width, src_height, dst_width, dst_height)
}
}
}
fn nearest_neighbor(
data: &[u8],
src_width: u32,
src_height: u32,
dst_width: u32,
dst_height: u32,
) -> WasmResult<Vec<u8>> {
let mut output = vec![0u8; (dst_width * dst_height * 4) as usize];
let x_ratio = src_width as f64 / dst_width as f64;
let y_ratio = src_height as f64 / dst_height as f64;
for y in 0..dst_height {
for x in 0..dst_width {
let src_x = (x as f64 * x_ratio) as u32;
let src_y = (y as f64 * y_ratio) as u32;
let src_idx = ((src_y * src_width + src_x) * 4) as usize;
let dst_idx = ((y * dst_width + x) * 4) as usize;
output[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
}
}
Ok(output)
}
fn bilinear(
data: &[u8],
src_width: u32,
src_height: u32,
dst_width: u32,
dst_height: u32,
) -> WasmResult<Vec<u8>> {
let mut output = vec![0u8; (dst_width * dst_height * 4) as usize];
let x_ratio = (src_width - 1) as f64 / dst_width as f64;
let y_ratio = (src_height - 1) as f64 / dst_height as f64;
for y in 0..dst_height {
for x in 0..dst_width {
let src_x = x as f64 * x_ratio;
let src_y = y as f64 * y_ratio;
let x1 = src_x.floor() as u32;
let y1 = src_y.floor() as u32;
let x2 = (x1 + 1).min(src_width - 1);
let y2 = (y1 + 1).min(src_height - 1);
let dx = src_x - x1 as f64;
let dy = src_y - y1 as f64;
let dst_idx = ((y * dst_width + x) * 4) as usize;
for c in 0..4 {
let p11 = data[((y1 * src_width + x1) * 4 + c) as usize];
let p21 = data[((y1 * src_width + x2) * 4 + c) as usize];
let p12 = data[((y2 * src_width + x1) * 4 + c) as usize];
let p22 = data[((y2 * src_width + x2) * 4 + c) as usize];
let val = (1.0 - dx) * (1.0 - dy) * f64::from(p11)
+ dx * (1.0 - dy) * f64::from(p21)
+ (1.0 - dx) * dy * f64::from(p12)
+ dx * dy * f64::from(p22);
output[dst_idx + c as usize] = val.round() as u8;
}
}
}
Ok(output)
}
fn bicubic(
data: &[u8],
src_width: u32,
src_height: u32,
dst_width: u32,
dst_height: u32,
) -> WasmResult<Vec<u8>> {
Self::bilinear(data, src_width, src_height, dst_width, dst_height)
}
}
#[wasm_bindgen]
pub struct WasmImageProcessor;
#[wasm_bindgen]
impl WasmImageProcessor {
#[wasm_bindgen(js_name = createImageData)]
pub fn create_image_data(data: &[u8], width: u32, height: u32) -> Result<ImageData, JsValue> {
if data.len() != (width * height * 4) as usize {
return Err(JsValue::from_str("Invalid data size"));
}
let clamped = wasm_bindgen::Clamped(data);
ImageData::new_with_u8_clamped_array_and_sh(clamped, width, height)
}
#[wasm_bindgen(js_name = computeHistogram)]
pub fn compute_histogram(data: &[u8], width: u32, height: u32) -> Result<String, JsValue> {
let hist = Histogram::from_rgba(data, width, height)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
hist.to_json_string(width, height)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = computeHistogramWithBins)]
pub fn compute_histogram_with_bins(
data: &[u8],
width: u32,
height: u32,
bin_edges: &[f64],
) -> Result<String, JsValue> {
let custom_hist = Histogram::from_rgba_with_bins(data, width, height, bin_edges)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&custom_hist).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = computeStats)]
pub fn compute_stats(data: &[u8], width: u32, height: u32) -> Result<String, JsValue> {
let stats = ImageStats::from_rgba(data, width, height)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&stats).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = linearStretch)]
pub fn linear_stretch(data: &mut [u8], width: u32, height: u32) -> Result<(), JsValue> {
ImageProcessor::linear_stretch(data, width, height)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = histogramEqualization)]
pub fn histogram_equalization(data: &mut [u8], width: u32, height: u32) -> Result<(), JsValue> {
ImageProcessor::histogram_equalization(data, width, height)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rgb_to_gray() {
let rgb = Rgb::new(128, 128, 128);
assert_eq!(rgb.to_gray(), 128);
let black = Rgb::new(0, 0, 0);
assert_eq!(black.to_gray(), 0);
let white = Rgb::new(255, 255, 255);
assert_eq!(white.to_gray(), 255);
}
#[test]
fn test_rgb_to_hsv() {
let red = Rgb::new(255, 0, 0);
let hsv = red.to_hsv();
assert!((hsv.h - 0.0).abs() < 1.0);
assert!((hsv.s - 1.0).abs() < 0.01);
assert!((hsv.v - 1.0).abs() < 0.01);
}
#[test]
fn test_hsv_to_rgb() {
let hsv = Hsv::new(0.0, 1.0, 1.0);
let rgb = hsv.to_rgb();
assert_eq!(rgb.r, 255);
assert!(rgb.g < 5);
assert!(rgb.b < 5);
}
#[test]
fn test_histogram() {
let data = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 128, 128, 128, 255, ];
let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
assert_eq!(hist.red[255], 1);
assert_eq!(hist.green[255], 1);
assert_eq!(hist.blue[255], 1);
}
#[test]
fn test_image_stats() {
let data = vec![
0, 0, 0, 255, 128, 128, 128, 255, 255, 255, 255, 255, 128, 128, 128, 255,
];
let stats = ImageStats::from_rgba(&data, 2, 2).expect("Stats computation failed");
assert_eq!(stats.min, 0);
assert_eq!(stats.max, 255);
}
#[test]
fn test_brightness_adjustment() {
let mut data = vec![100, 100, 100, 255];
ImageProcessor::adjust_brightness(&mut data, 50);
assert_eq!(data[0], 150);
assert_eq!(data[1], 150);
assert_eq!(data[2], 150);
}
#[test]
fn test_grayscale_conversion() {
let mut data = vec![255, 0, 0, 255]; ImageProcessor::to_grayscale(&mut data);
assert_eq!(data[0], data[1]);
assert_eq!(data[1], data[2]);
}
#[test]
fn test_nearest_neighbor_resample() {
let data = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
];
let resampled = Resampler::nearest_neighbor(&data, 2, 2, 4, 4).expect("Resample failed");
assert_eq!(resampled.len(), 4 * 4 * 4);
}
#[test]
fn test_histogram_json_serialization() {
let data = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 128, 128, 128, 255, ];
let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
let json_result = hist.to_json_string(2, 2);
assert!(json_result.is_ok(), "JSON serialization should succeed");
let json_str = json_result.expect("Should have JSON string");
let parsed: serde_json::Value =
serde_json::from_str(&json_str).expect("Should parse as valid JSON");
assert_eq!(parsed["width"], 2);
assert_eq!(parsed["height"], 2);
assert_eq!(parsed["total_pixels"], 4);
assert!(parsed["red"]["bins"].is_array());
assert_eq!(parsed["red"]["bins"].as_array().map(|a| a.len()), Some(256));
assert!(parsed["red"]["count"].as_u64().is_some());
assert!(parsed["luminance"]["min"].is_u64());
assert!(parsed["luminance"]["max"].is_u64());
assert!(parsed["luminance"]["mean"].is_f64());
assert!(parsed["luminance"]["std_dev"].is_f64());
}
#[test]
fn test_histogram_json_struct() {
let data = vec![
100, 100, 100, 255, 100, 100, 100, 255, 200, 200, 200, 255, 200, 200, 200, 255, ];
let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
let hist_json = hist.to_json(2, 2);
assert_eq!(hist_json.width, 2);
assert_eq!(hist_json.height, 2);
assert_eq!(hist_json.total_pixels, 4);
assert_eq!(hist_json.red.bins.len(), 256);
assert_eq!(hist_json.green.bins.len(), 256);
assert_eq!(hist_json.blue.bins.len(), 256);
assert_eq!(hist_json.luminance.bins.len(), 256);
assert_eq!(hist_json.red.count, 4);
assert_eq!(hist_json.green.count, 4);
assert_eq!(hist_json.blue.count, 4);
assert_eq!(hist_json.luminance.count, 4);
assert_eq!(hist_json.red.bins[100], 2);
assert_eq!(hist_json.red.bins[200], 2);
}
#[test]
fn test_histogram_std_dev() {
let data = vec![
128, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255,
];
let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
let std_dev = hist.std_dev();
assert!(
std_dev.abs() < f64::EPSILON,
"Uniform values should have zero std_dev"
);
let varied_data = vec![
0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, ];
let varied_hist =
Histogram::from_rgba(&varied_data, 2, 2).expect("Histogram computation failed");
let varied_std_dev = varied_hist.std_dev();
assert!(
varied_std_dev > 100.0,
"High variation should have high std_dev"
);
}
#[test]
fn test_channel_histogram_statistics() {
let data = vec![
0, 0, 0, 255, 64, 64, 64, 255, 192, 192, 192, 255, 255, 255, 255, 255, ];
let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
let hist_json = hist.to_json(2, 2);
assert_eq!(hist_json.luminance.min, 0);
assert_eq!(hist_json.luminance.max, 255);
assert!(
(hist_json.luminance.mean - 127.75).abs() < 1.0,
"Mean should be approximately 127.75, got {}",
hist_json.luminance.mean
);
}
#[test]
fn test_custom_bin_histogram() {
let data = vec![
25, 25, 25, 255, 75, 75, 75, 255, 125, 125, 125, 255, 175, 175, 175, 255, ];
let bin_edges = vec![0.0, 50.0, 100.0, 150.0, 200.0, 256.0];
let custom_hist = Histogram::from_rgba_with_bins(&data, 2, 2, &bin_edges)
.expect("Custom bin histogram computation failed");
assert_eq!(custom_hist.num_bins, 5);
assert_eq!(custom_hist.bins.len(), 5);
assert_eq!(custom_hist.bins[0], 1); assert_eq!(custom_hist.bins[1], 1); assert_eq!(custom_hist.bins[2], 1); assert_eq!(custom_hist.bins[3], 1); assert_eq!(custom_hist.bins[4], 0);
assert_eq!(custom_hist.min, 25.0);
assert_eq!(custom_hist.max, 175.0);
}
#[test]
fn test_histogram_pretty_json() {
let data = vec![128, 128, 128, 255, 128, 128, 128, 255];
let hist = Histogram::from_rgba(&data, 2, 1).expect("Histogram computation failed");
let hist_json = hist.to_json(2, 1);
let pretty_json = hist_json.to_json_string_pretty();
assert!(
pretty_json.is_ok(),
"Pretty JSON serialization should succeed"
);
let pretty_str = pretty_json.expect("Should have pretty JSON string");
assert!(
pretty_str.contains('\n'),
"Pretty JSON should contain newlines"
);
assert!(
pretty_str.contains(" "),
"Pretty JSON should contain indentation"
);
}
#[test]
fn test_empty_histogram() {
let data = vec![128, 128, 128, 255];
let hist = Histogram::from_rgba(&data, 1, 1).expect("Histogram computation failed");
let hist_json = hist.to_json(1, 1);
assert_eq!(hist_json.total_pixels, 1);
assert_eq!(hist_json.luminance.count, 1);
assert_eq!(hist_json.luminance.min, 128);
assert_eq!(hist_json.luminance.max, 128);
assert!(
hist_json.luminance.std_dev.abs() < f64::EPSILON,
"Single pixel should have zero std_dev"
);
}
}