1use serde::{Deserialize, Serialize};
102use wasm_bindgen::prelude::*;
103use web_sys::ImageData;
104
105use crate::error::{CanvasError, WasmError, WasmResult};
106
107#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
109pub struct Rgb {
110 pub r: u8,
112 pub g: u8,
114 pub b: u8,
116}
117
118impl Rgb {
119 pub const fn new(r: u8, g: u8, b: u8) -> Self {
121 Self { r, g, b }
122 }
123
124 pub const fn from_gray(value: u8) -> Self {
126 Self::new(value, value, value)
127 }
128
129 pub fn to_gray(&self) -> u8 {
131 ((77 * u16::from(self.r) + 150 * u16::from(self.g) + 29 * u16::from(self.b)) / 256) as u8
133 }
134
135 pub fn to_hsv(&self) -> Hsv {
137 let r = f64::from(self.r) / 255.0;
138 let g = f64::from(self.g) / 255.0;
139 let b = f64::from(self.b) / 255.0;
140
141 let max = r.max(g).max(b);
142 let min = r.min(g).min(b);
143 let delta = max - min;
144
145 let h = if delta < f64::EPSILON {
146 0.0
147 } else if (max - r).abs() < f64::EPSILON {
148 60.0 * (((g - b) / delta) % 6.0)
149 } else if (max - g).abs() < f64::EPSILON {
150 60.0 * (((b - r) / delta) + 2.0)
151 } else {
152 60.0 * (((r - g) / delta) + 4.0)
153 };
154
155 let s = if max < f64::EPSILON { 0.0 } else { delta / max };
156 let v = max;
157
158 Hsv {
159 h: if h < 0.0 { h + 360.0 } else { h },
160 s,
161 v,
162 }
163 }
164
165 pub fn to_ycbcr(&self) -> YCbCr {
167 let r = f64::from(self.r);
168 let g = f64::from(self.g);
169 let b = f64::from(self.b);
170
171 let y = 0.299 * r + 0.587 * g + 0.114 * b;
172 let cb = 128.0 + (-0.168736 * r - 0.331264 * g + 0.5 * b);
173 let cr = 128.0 + (0.5 * r - 0.418688 * g - 0.081312 * b);
174
175 YCbCr {
176 y: y.clamp(0.0, 255.0) as u8,
177 cb: cb.clamp(0.0, 255.0) as u8,
178 cr: cr.clamp(0.0, 255.0) as u8,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq)]
185pub struct Hsv {
186 pub h: f64,
188 pub s: f64,
190 pub v: f64,
192}
193
194impl Hsv {
195 pub const fn new(h: f64, s: f64, v: f64) -> Self {
197 Self { h, s, v }
198 }
199
200 pub fn to_rgb(&self) -> Rgb {
202 let c = self.v * self.s;
203 let h_prime = self.h / 60.0;
204 let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
205 let m = self.v - c;
206
207 let (r, g, b) = if h_prime < 1.0 {
208 (c, x, 0.0)
209 } else if h_prime < 2.0 {
210 (x, c, 0.0)
211 } else if h_prime < 3.0 {
212 (0.0, c, x)
213 } else if h_prime < 4.0 {
214 (0.0, x, c)
215 } else if h_prime < 5.0 {
216 (x, 0.0, c)
217 } else {
218 (c, 0.0, x)
219 };
220
221 Rgb::new(
222 ((r + m) * 255.0).round() as u8,
223 ((g + m) * 255.0).round() as u8,
224 ((b + m) * 255.0).round() as u8,
225 )
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq)]
231pub struct YCbCr {
232 pub y: u8,
234 pub cb: u8,
236 pub cr: u8,
238}
239
240impl YCbCr {
241 pub const fn new(y: u8, cb: u8, cr: u8) -> Self {
243 Self { y, cb, cr }
244 }
245
246 pub fn to_rgb(&self) -> Rgb {
248 let y = f64::from(self.y);
249 let cb = f64::from(self.cb) - 128.0;
250 let cr = f64::from(self.cr) - 128.0;
251
252 let r = y + 1.402 * cr;
253 let g = y - 0.344136 * cb - 0.714136 * cr;
254 let b = y + 1.772 * cb;
255
256 Rgb::new(
257 r.clamp(0.0, 255.0) as u8,
258 g.clamp(0.0, 255.0) as u8,
259 b.clamp(0.0, 255.0) as u8,
260 )
261 }
262}
263
264#[derive(Debug, Clone)]
266pub struct Histogram {
267 pub red: [u32; 256],
269 pub green: [u32; 256],
271 pub blue: [u32; 256],
273 pub luminance: [u32; 256],
275}
276
277impl Histogram {
278 pub const fn new() -> Self {
280 Self {
281 red: [0; 256],
282 green: [0; 256],
283 blue: [0; 256],
284 luminance: [0; 256],
285 }
286 }
287
288 pub fn from_rgba(data: &[u8], width: u32, height: u32) -> WasmResult<Self> {
290 if width == 0 || height == 0 || data.is_empty() {
291 return Err(WasmError::Canvas(CanvasError::InvalidParameter(
292 "Width, height, and data must be non-empty".to_string(),
293 )));
294 }
295
296 let expected_len = (width as usize) * (height as usize) * 4;
297 if data.len() != expected_len {
298 return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
299 expected: expected_len,
300 actual: data.len(),
301 }));
302 }
303
304 let mut hist = Self::new();
305
306 for chunk in data.chunks_exact(4) {
307 let r = chunk[0];
308 let g = chunk[1];
309 let b = chunk[2];
310
311 hist.red[r as usize] += 1;
312 hist.green[g as usize] += 1;
313 hist.blue[b as usize] += 1;
314
315 let lum = Rgb::new(r, g, b).to_gray();
316 hist.luminance[lum as usize] += 1;
317 }
318
319 Ok(hist)
320 }
321
322 pub fn min_value(&self) -> u8 {
324 for (i, &count) in self.luminance.iter().enumerate() {
325 if count > 0 {
326 return i as u8;
327 }
328 }
329 0
330 }
331
332 pub fn max_value(&self) -> u8 {
334 for (i, &count) in self.luminance.iter().enumerate().rev() {
335 if count > 0 {
336 return i as u8;
337 }
338 }
339 255
340 }
341
342 pub fn mean(&self) -> f64 {
344 let total: u64 = self.luminance.iter().map(|&x| u64::from(x)).sum();
345 if total == 0 {
346 return 0.0;
347 }
348
349 let weighted_sum: u64 = self
350 .luminance
351 .iter()
352 .enumerate()
353 .map(|(val, &count)| val as u64 * u64::from(count))
354 .sum();
355
356 weighted_sum as f64 / total as f64
357 }
358
359 pub fn median(&self) -> u8 {
361 let total: u64 = self.luminance.iter().map(|&x| u64::from(x)).sum();
362 if total == 0 {
363 return 0;
364 }
365
366 let target = total / 2;
367 let mut cumulative = 0u64;
368
369 for (i, &count) in self.luminance.iter().enumerate() {
370 cumulative += u64::from(count);
371 if cumulative >= target {
372 return i as u8;
373 }
374 }
375
376 255
377 }
378}
379
380impl Default for Histogram {
381 fn default() -> Self {
382 Self::new()
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct ChannelHistogramJson {
389 pub bins: Vec<u32>,
391 pub min: u8,
393 pub max: u8,
395 pub mean: f64,
397 pub median: u8,
399 pub std_dev: f64,
401 pub count: u64,
403}
404
405impl ChannelHistogramJson {
406 fn from_histogram_array(hist: &[u32; 256]) -> Self {
408 let count: u64 = hist.iter().map(|&x| u64::from(x)).sum();
409
410 let min = hist
412 .iter()
413 .enumerate()
414 .find(|&(_, &c)| c > 0)
415 .map(|(i, _)| i as u8)
416 .unwrap_or(0);
417
418 let max = hist
420 .iter()
421 .enumerate()
422 .rev()
423 .find(|&(_, &c)| c > 0)
424 .map(|(i, _)| i as u8)
425 .unwrap_or(255);
426
427 let mean = if count > 0 {
429 let weighted_sum: u64 = hist
430 .iter()
431 .enumerate()
432 .map(|(val, &c)| val as u64 * u64::from(c))
433 .sum();
434 weighted_sum as f64 / count as f64
435 } else {
436 0.0
437 };
438
439 let median = if count > 0 {
441 let target = count / 2;
442 let mut cumulative = 0u64;
443 let mut median_val = 0u8;
444 for (i, &c) in hist.iter().enumerate() {
445 cumulative += u64::from(c);
446 if cumulative >= target {
447 median_val = i as u8;
448 break;
449 }
450 }
451 median_val
452 } else {
453 0
454 };
455
456 let std_dev = if count > 0 {
458 let variance: f64 = hist
459 .iter()
460 .enumerate()
461 .map(|(val, &c)| {
462 let diff = val as f64 - mean;
463 diff * diff * f64::from(c)
464 })
465 .sum::<f64>()
466 / count as f64;
467 variance.sqrt()
468 } else {
469 0.0
470 };
471
472 Self {
473 bins: hist.to_vec(),
474 min,
475 max,
476 mean,
477 median,
478 std_dev,
479 count,
480 }
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct CustomBinHistogramJson {
487 pub bins: Vec<u32>,
489 pub bin_edges: Vec<f64>,
491 pub min: f64,
493 pub max: f64,
495 pub num_bins: usize,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct HistogramJson {
502 pub width: u32,
504 pub height: u32,
506 pub total_pixels: u64,
508 pub red: ChannelHistogramJson,
510 pub green: ChannelHistogramJson,
512 pub blue: ChannelHistogramJson,
514 pub luminance: ChannelHistogramJson,
516}
517
518impl HistogramJson {
519 pub fn from_histogram(hist: &Histogram, width: u32, height: u32) -> Self {
521 Self {
522 width,
523 height,
524 total_pixels: u64::from(width) * u64::from(height),
525 red: ChannelHistogramJson::from_histogram_array(&hist.red),
526 green: ChannelHistogramJson::from_histogram_array(&hist.green),
527 blue: ChannelHistogramJson::from_histogram_array(&hist.blue),
528 luminance: ChannelHistogramJson::from_histogram_array(&hist.luminance),
529 }
530 }
531
532 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
534 serde_json::to_string(self)
535 }
536
537 pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
539 serde_json::to_string_pretty(self)
540 }
541}
542
543impl Histogram {
544 pub fn std_dev(&self) -> f64 {
546 let total: u64 = self.luminance.iter().map(|&x| u64::from(x)).sum();
547 if total == 0 {
548 return 0.0;
549 }
550
551 let mean = self.mean();
552 let variance: f64 = self
553 .luminance
554 .iter()
555 .enumerate()
556 .map(|(val, &count)| {
557 let diff = val as f64 - mean;
558 diff * diff * f64::from(count)
559 })
560 .sum::<f64>()
561 / total as f64;
562
563 variance.sqrt()
564 }
565
566 pub fn to_json(&self, width: u32, height: u32) -> HistogramJson {
568 HistogramJson::from_histogram(self, width, height)
569 }
570
571 pub fn to_json_string(&self, width: u32, height: u32) -> Result<String, serde_json::Error> {
573 self.to_json(width, height).to_json_string()
574 }
575
576 pub fn from_rgba_with_bins(
580 data: &[u8],
581 width: u32,
582 height: u32,
583 bin_edges: &[f64],
584 ) -> WasmResult<CustomBinHistogramJson> {
585 let expected_len = (width as usize) * (height as usize) * 4;
586 if data.len() != expected_len {
587 return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
588 expected: expected_len,
589 actual: data.len(),
590 }));
591 }
592
593 if bin_edges.len() < 2 {
594 return Err(WasmError::Canvas(CanvasError::InvalidParameter(
595 "bin_edges must have at least 2 elements".to_string(),
596 )));
597 }
598
599 let num_bins = bin_edges.len() - 1;
600 let mut bins = vec![0u32; num_bins];
601 let mut min_val = f64::MAX;
602 let mut max_val = f64::MIN;
603
604 for chunk in data.chunks_exact(4) {
605 let r = chunk[0];
607 let g = chunk[1];
608 let b = chunk[2];
609 let lum = Rgb::new(r, g, b).to_gray();
610 let lum_f = f64::from(lum);
611
612 min_val = min_val.min(lum_f);
613 max_val = max_val.max(lum_f);
614
615 for i in 0..num_bins {
617 if lum_f >= bin_edges[i] && lum_f < bin_edges[i + 1] {
618 bins[i] += 1;
619 break;
620 }
621 }
622 if (lum_f - bin_edges[num_bins]).abs() < f64::EPSILON {
624 bins[num_bins - 1] += 1;
625 }
626 }
627
628 Ok(CustomBinHistogramJson {
629 bins,
630 bin_edges: bin_edges.to_vec(),
631 min: if min_val == f64::MAX { 0.0 } else { min_val },
632 max: if max_val == f64::MIN { 255.0 } else { max_val },
633 num_bins,
634 })
635 }
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct ImageStats {
641 pub width: u32,
643 pub height: u32,
645 pub min: u8,
647 pub max: u8,
649 pub mean: f64,
651 pub median: u8,
653 pub std_dev: f64,
655}
656
657impl ImageStats {
658 pub fn from_rgba(data: &[u8], width: u32, height: u32) -> WasmResult<Self> {
660 let hist = Histogram::from_rgba(data, width, height)?;
661
662 let min = hist.min_value();
663 let max = hist.max_value();
664 let mean = hist.mean();
665 let median = hist.median();
666
667 let total: u64 = hist.luminance.iter().map(|&x| u64::from(x)).sum();
669 let variance: f64 = hist
670 .luminance
671 .iter()
672 .enumerate()
673 .map(|(val, &count)| {
674 let diff = val as f64 - mean;
675 diff * diff * f64::from(count)
676 })
677 .sum::<f64>()
678 / total as f64;
679
680 let std_dev = variance.sqrt();
681
682 Ok(Self {
683 width,
684 height,
685 min,
686 max,
687 mean,
688 median,
689 std_dev,
690 })
691 }
692}
693
694#[derive(Debug, Clone, Copy, PartialEq, Eq)]
696pub enum ContrastMethod {
697 LinearStretch,
699 HistogramEqualization,
701 AdaptiveHistogramEqualization,
703}
704
705pub struct ImageProcessor;
707
708impl ImageProcessor {
709 pub fn enhance_contrast(
711 data: &mut [u8],
712 width: u32,
713 height: u32,
714 method: ContrastMethod,
715 ) -> WasmResult<()> {
716 match method {
717 ContrastMethod::LinearStretch => Self::linear_stretch(data, width, height),
718 ContrastMethod::HistogramEqualization => {
719 Self::histogram_equalization(data, width, height)
720 }
721 ContrastMethod::AdaptiveHistogramEqualization => {
722 Self::adaptive_histogram_equalization(data, width, height)
723 }
724 }
725 }
726
727 fn linear_stretch(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
729 let hist = Histogram::from_rgba(data, width, height)?;
730 let min = hist.min_value();
731 let max = hist.max_value();
732
733 if min == max {
734 return Ok(());
735 }
736
737 let scale = 255.0 / (max - min) as f64;
738
739 for chunk in data.chunks_exact_mut(4) {
740 chunk[0] = ((chunk[0].saturating_sub(min)) as f64 * scale) as u8;
741 chunk[1] = ((chunk[1].saturating_sub(min)) as f64 * scale) as u8;
742 chunk[2] = ((chunk[2].saturating_sub(min)) as f64 * scale) as u8;
743 }
744
745 Ok(())
746 }
747
748 fn histogram_equalization(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
750 let hist = Histogram::from_rgba(data, width, height)?;
751 let total_pixels = (width as usize) * (height as usize);
752
753 let mut cdf = [0u32; 256];
755 cdf[0] = hist.luminance[0];
756 for i in 1..256 {
757 cdf[i] = cdf[i - 1] + hist.luminance[i];
758 }
759
760 let cdf_min = cdf.iter().find(|&&x| x > 0).copied().unwrap_or(0);
762
763 let mut lut = [0u8; 256];
765 for i in 0..256 {
766 if total_pixels > cdf_min as usize {
767 lut[i] = (((cdf[i] - cdf_min) as f64 / (total_pixels - cdf_min as usize) as f64)
768 * 255.0) as u8;
769 }
770 }
771
772 for chunk in data.chunks_exact_mut(4) {
774 let lum = Rgb::new(chunk[0], chunk[1], chunk[2]).to_gray();
775 let new_lum = lut[lum as usize];
776
777 if lum > 0 {
779 let scale = new_lum as f64 / lum as f64;
780 chunk[0] = ((chunk[0] as f64 * scale).min(255.0)) as u8;
781 chunk[1] = ((chunk[1] as f64 * scale).min(255.0)) as u8;
782 chunk[2] = ((chunk[2] as f64 * scale).min(255.0)) as u8;
783 }
784 }
785
786 Ok(())
787 }
788
789 fn adaptive_histogram_equalization(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
791 Self::histogram_equalization(data, width, height)
794 }
795
796 pub fn adjust_brightness(data: &mut [u8], delta: i32) {
798 for chunk in data.chunks_exact_mut(4) {
799 chunk[0] = (chunk[0] as i32 + delta).clamp(0, 255) as u8;
800 chunk[1] = (chunk[1] as i32 + delta).clamp(0, 255) as u8;
801 chunk[2] = (chunk[2] as i32 + delta).clamp(0, 255) as u8;
802 }
803 }
804
805 pub fn gamma_correction(data: &mut [u8], gamma: f64) {
807 let inv_gamma = 1.0 / gamma;
808 let mut lut = [0u8; 256];
809 for i in 0..256 {
810 lut[i] = ((i as f64 / 255.0).powf(inv_gamma) * 255.0) as u8;
811 }
812
813 for chunk in data.chunks_exact_mut(4) {
814 chunk[0] = lut[chunk[0] as usize];
815 chunk[1] = lut[chunk[1] as usize];
816 chunk[2] = lut[chunk[2] as usize];
817 }
818 }
819
820 pub fn adjust_contrast(data: &mut [u8], factor: f64) {
823 let factor = factor.max(0.0);
824
825 for chunk in data.chunks_exact_mut(4) {
826 for i in 0..3 {
827 let val = chunk[i] as f64;
828 let adjusted = ((val - 128.0) * factor + 128.0).clamp(0.0, 255.0);
829 chunk[i] = adjusted as u8;
830 }
831 }
832 }
833
834 pub fn adjust_saturation(data: &mut [u8], factor: f64) {
837 let factor = factor.max(0.0);
838
839 for chunk in data.chunks_exact_mut(4) {
840 let rgb = Rgb::new(chunk[0], chunk[1], chunk[2]);
841 let mut hsv = rgb.to_hsv();
842
843 hsv.s = (hsv.s * factor).clamp(0.0, 1.0);
845
846 let adjusted = hsv.to_rgb();
847 chunk[0] = adjusted.r;
848 chunk[1] = adjusted.g;
849 chunk[2] = adjusted.b;
850 }
851 }
852
853 pub fn to_grayscale(data: &mut [u8]) {
855 for chunk in data.chunks_exact_mut(4) {
856 let gray = Rgb::new(chunk[0], chunk[1], chunk[2]).to_gray();
857 chunk[0] = gray;
858 chunk[1] = gray;
859 chunk[2] = gray;
860 }
861 }
862
863 pub fn invert(data: &mut [u8]) {
865 for chunk in data.chunks_exact_mut(4) {
866 chunk[0] = 255 - chunk[0];
867 chunk[1] = 255 - chunk[1];
868 chunk[2] = 255 - chunk[2];
869 }
870 }
871
872 pub fn convolve_3x3(
874 data: &[u8],
875 width: u32,
876 height: u32,
877 kernel: &[f32; 9],
878 ) -> WasmResult<Vec<u8>> {
879 let w = width as usize;
880 let h = height as usize;
881 let mut output = vec![0u8; w * h * 4];
882
883 for y in 1..h - 1 {
884 for x in 1..w - 1 {
885 for c in 0..3 {
886 let mut sum = 0.0;
887
888 for ky in 0..3 {
889 for kx in 0..3 {
890 let px = x + kx - 1;
891 let py = y + ky - 1;
892 let idx = (py * w + px) * 4 + c;
893 sum += f32::from(data[idx]) * kernel[ky * 3 + kx];
894 }
895 }
896
897 let out_idx = (y * w + x) * 4 + c;
898 output[out_idx] = sum.clamp(0.0, 255.0) as u8;
899 }
900
901 let out_idx = (y * w + x) * 4 + 3;
903 let in_idx = (y * w + x) * 4 + 3;
904 output[out_idx] = data[in_idx];
905 }
906 }
907
908 Ok(output)
909 }
910
911 pub fn gaussian_blur(data: &[u8], width: u32, height: u32) -> WasmResult<Vec<u8>> {
913 #[allow(clippy::excessive_precision)]
914 let kernel = [
915 1.0 / 16.0,
916 2.0 / 16.0,
917 1.0 / 16.0,
918 2.0 / 16.0,
919 4.0 / 16.0,
920 2.0 / 16.0,
921 1.0 / 16.0,
922 2.0 / 16.0,
923 1.0 / 16.0,
924 ];
925 Self::convolve_3x3(data, width, height, &kernel)
926 }
927
928 pub fn edge_detection(data: &[u8], width: u32, height: u32) -> WasmResult<Vec<u8>> {
930 let sobel_x = [-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
931 let sobel_y = [-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0];
932
933 let gx = Self::convolve_3x3(data, width, height, &sobel_x)?;
934 let gy = Self::convolve_3x3(data, width, height, &sobel_y)?;
935
936 let mut output = vec![0u8; gx.len()];
937 for i in (0..gx.len()).step_by(4) {
938 for c in 0..3 {
939 let gx_val = f64::from(gx[i + c]);
940 let gy_val = f64::from(gy[i + c]);
941 let magnitude = (gx_val * gx_val + gy_val * gy_val).sqrt();
942 output[i + c] = magnitude.min(255.0) as u8;
943 }
944 output[i + 3] = 255; }
946
947 Ok(output)
948 }
949
950 pub fn sharpen(data: &[u8], width: u32, height: u32) -> WasmResult<Vec<u8>> {
952 let kernel = [0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0];
953 Self::convolve_3x3(data, width, height, &kernel)
954 }
955}
956
957#[derive(Debug, Clone, Copy, PartialEq, Eq)]
959pub enum ResampleMethod {
960 NearestNeighbor,
962 Bilinear,
964 Bicubic,
966}
967
968pub struct Resampler;
970
971impl Resampler {
972 pub fn resample(
974 data: &[u8],
975 src_width: u32,
976 src_height: u32,
977 dst_width: u32,
978 dst_height: u32,
979 method: ResampleMethod,
980 ) -> WasmResult<Vec<u8>> {
981 match method {
982 ResampleMethod::NearestNeighbor => {
983 Self::nearest_neighbor(data, src_width, src_height, dst_width, dst_height)
984 }
985 ResampleMethod::Bilinear => {
986 Self::bilinear(data, src_width, src_height, dst_width, dst_height)
987 }
988 ResampleMethod::Bicubic => {
989 Self::bicubic(data, src_width, src_height, dst_width, dst_height)
990 }
991 }
992 }
993
994 fn nearest_neighbor(
996 data: &[u8],
997 src_width: u32,
998 src_height: u32,
999 dst_width: u32,
1000 dst_height: u32,
1001 ) -> WasmResult<Vec<u8>> {
1002 let mut output = vec![0u8; (dst_width * dst_height * 4) as usize];
1003
1004 let x_ratio = src_width as f64 / dst_width as f64;
1005 let y_ratio = src_height as f64 / dst_height as f64;
1006
1007 for y in 0..dst_height {
1008 for x in 0..dst_width {
1009 let src_x = (x as f64 * x_ratio) as u32;
1010 let src_y = (y as f64 * y_ratio) as u32;
1011
1012 let src_idx = ((src_y * src_width + src_x) * 4) as usize;
1013 let dst_idx = ((y * dst_width + x) * 4) as usize;
1014
1015 output[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
1016 }
1017 }
1018
1019 Ok(output)
1020 }
1021
1022 fn bilinear(
1024 data: &[u8],
1025 src_width: u32,
1026 src_height: u32,
1027 dst_width: u32,
1028 dst_height: u32,
1029 ) -> WasmResult<Vec<u8>> {
1030 let mut output = vec![0u8; (dst_width * dst_height * 4) as usize];
1031
1032 let x_ratio = (src_width - 1) as f64 / dst_width as f64;
1033 let y_ratio = (src_height - 1) as f64 / dst_height as f64;
1034
1035 for y in 0..dst_height {
1036 for x in 0..dst_width {
1037 let src_x = x as f64 * x_ratio;
1038 let src_y = y as f64 * y_ratio;
1039
1040 let x1 = src_x.floor() as u32;
1041 let y1 = src_y.floor() as u32;
1042 let x2 = (x1 + 1).min(src_width - 1);
1043 let y2 = (y1 + 1).min(src_height - 1);
1044
1045 let dx = src_x - x1 as f64;
1046 let dy = src_y - y1 as f64;
1047
1048 let dst_idx = ((y * dst_width + x) * 4) as usize;
1049
1050 for c in 0..4 {
1051 let p11 = data[((y1 * src_width + x1) * 4 + c) as usize];
1052 let p21 = data[((y1 * src_width + x2) * 4 + c) as usize];
1053 let p12 = data[((y2 * src_width + x1) * 4 + c) as usize];
1054 let p22 = data[((y2 * src_width + x2) * 4 + c) as usize];
1055
1056 let val = (1.0 - dx) * (1.0 - dy) * f64::from(p11)
1057 + dx * (1.0 - dy) * f64::from(p21)
1058 + (1.0 - dx) * dy * f64::from(p12)
1059 + dx * dy * f64::from(p22);
1060
1061 output[dst_idx + c as usize] = val.round() as u8;
1062 }
1063 }
1064 }
1065
1066 Ok(output)
1067 }
1068
1069 fn bicubic(
1071 data: &[u8],
1072 src_width: u32,
1073 src_height: u32,
1074 dst_width: u32,
1075 dst_height: u32,
1076 ) -> WasmResult<Vec<u8>> {
1077 Self::bilinear(data, src_width, src_height, dst_width, dst_height)
1079 }
1080}
1081
1082#[wasm_bindgen]
1084pub struct WasmImageProcessor;
1085
1086#[wasm_bindgen]
1087impl WasmImageProcessor {
1088 #[wasm_bindgen(js_name = createImageData)]
1090 pub fn create_image_data(data: &[u8], width: u32, height: u32) -> Result<ImageData, JsValue> {
1091 if data.len() != (width * height * 4) as usize {
1092 return Err(JsValue::from_str("Invalid data size"));
1093 }
1094
1095 let clamped = wasm_bindgen::Clamped(data);
1096 ImageData::new_with_u8_clamped_array_and_sh(clamped, width, height)
1097 }
1098
1099 #[wasm_bindgen(js_name = computeHistogram)]
1129 pub fn compute_histogram(data: &[u8], width: u32, height: u32) -> Result<String, JsValue> {
1130 let hist = Histogram::from_rgba(data, width, height)
1131 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1132
1133 hist.to_json_string(width, height)
1134 .map_err(|e| JsValue::from_str(&e.to_string()))
1135 }
1136
1137 #[wasm_bindgen(js_name = computeHistogramWithBins)]
1157 pub fn compute_histogram_with_bins(
1158 data: &[u8],
1159 width: u32,
1160 height: u32,
1161 bin_edges: &[f64],
1162 ) -> Result<String, JsValue> {
1163 let custom_hist = Histogram::from_rgba_with_bins(data, width, height, bin_edges)
1164 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1165
1166 serde_json::to_string(&custom_hist).map_err(|e| JsValue::from_str(&e.to_string()))
1167 }
1168
1169 #[wasm_bindgen(js_name = computeStats)]
1171 pub fn compute_stats(data: &[u8], width: u32, height: u32) -> Result<String, JsValue> {
1172 let stats = ImageStats::from_rgba(data, width, height)
1173 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1174
1175 serde_json::to_string(&stats).map_err(|e| JsValue::from_str(&e.to_string()))
1176 }
1177
1178 #[wasm_bindgen(js_name = linearStretch)]
1180 pub fn linear_stretch(data: &mut [u8], width: u32, height: u32) -> Result<(), JsValue> {
1181 ImageProcessor::linear_stretch(data, width, height)
1182 .map_err(|e| JsValue::from_str(&e.to_string()))
1183 }
1184
1185 #[wasm_bindgen(js_name = histogramEqualization)]
1187 pub fn histogram_equalization(data: &mut [u8], width: u32, height: u32) -> Result<(), JsValue> {
1188 ImageProcessor::histogram_equalization(data, width, height)
1189 .map_err(|e| JsValue::from_str(&e.to_string()))
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196
1197 #[test]
1198 fn test_rgb_to_gray() {
1199 let rgb = Rgb::new(128, 128, 128);
1200 assert_eq!(rgb.to_gray(), 128);
1201
1202 let black = Rgb::new(0, 0, 0);
1203 assert_eq!(black.to_gray(), 0);
1204
1205 let white = Rgb::new(255, 255, 255);
1206 assert_eq!(white.to_gray(), 255);
1207 }
1208
1209 #[test]
1210 fn test_rgb_to_hsv() {
1211 let red = Rgb::new(255, 0, 0);
1212 let hsv = red.to_hsv();
1213 assert!((hsv.h - 0.0).abs() < 1.0);
1214 assert!((hsv.s - 1.0).abs() < 0.01);
1215 assert!((hsv.v - 1.0).abs() < 0.01);
1216 }
1217
1218 #[test]
1219 fn test_hsv_to_rgb() {
1220 let hsv = Hsv::new(0.0, 1.0, 1.0);
1221 let rgb = hsv.to_rgb();
1222 assert_eq!(rgb.r, 255);
1223 assert!(rgb.g < 5);
1224 assert!(rgb.b < 5);
1225 }
1226
1227 #[test]
1228 fn test_histogram() {
1229 let data = vec![
1230 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 128, 128, 128, 255, ];
1235
1236 let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
1237 assert_eq!(hist.red[255], 1);
1238 assert_eq!(hist.green[255], 1);
1239 assert_eq!(hist.blue[255], 1);
1240 }
1241
1242 #[test]
1243 fn test_image_stats() {
1244 let data = vec![
1245 0, 0, 0, 255, 128, 128, 128, 255, 255, 255, 255, 255, 128, 128, 128, 255,
1246 ];
1247
1248 let stats = ImageStats::from_rgba(&data, 2, 2).expect("Stats computation failed");
1249 assert_eq!(stats.min, 0);
1250 assert_eq!(stats.max, 255);
1251 }
1252
1253 #[test]
1254 fn test_brightness_adjustment() {
1255 let mut data = vec![100, 100, 100, 255];
1256 ImageProcessor::adjust_brightness(&mut data, 50);
1257 assert_eq!(data[0], 150);
1258 assert_eq!(data[1], 150);
1259 assert_eq!(data[2], 150);
1260 }
1261
1262 #[test]
1263 fn test_grayscale_conversion() {
1264 let mut data = vec![255, 0, 0, 255]; ImageProcessor::to_grayscale(&mut data);
1266 assert_eq!(data[0], data[1]);
1268 assert_eq!(data[1], data[2]);
1269 }
1270
1271 #[test]
1272 fn test_nearest_neighbor_resample() {
1273 let data = vec![
1274 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
1275 ];
1276
1277 let resampled = Resampler::nearest_neighbor(&data, 2, 2, 4, 4).expect("Resample failed");
1278
1279 assert_eq!(resampled.len(), 4 * 4 * 4);
1280 }
1281
1282 #[test]
1283 fn test_histogram_json_serialization() {
1284 let data = vec![
1285 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 128, 128, 128, 255, ];
1290
1291 let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
1292 let json_result = hist.to_json_string(2, 2);
1293
1294 assert!(json_result.is_ok(), "JSON serialization should succeed");
1295
1296 let json_str = json_result.expect("Should have JSON string");
1297 let parsed: serde_json::Value =
1298 serde_json::from_str(&json_str).expect("Should parse as valid JSON");
1299
1300 assert_eq!(parsed["width"], 2);
1302 assert_eq!(parsed["height"], 2);
1303 assert_eq!(parsed["total_pixels"], 4);
1304
1305 assert!(parsed["red"]["bins"].is_array());
1307 assert_eq!(parsed["red"]["bins"].as_array().map(|a| a.len()), Some(256));
1308 assert!(parsed["red"]["count"].as_u64().is_some());
1309
1310 assert!(parsed["luminance"]["min"].is_u64());
1312 assert!(parsed["luminance"]["max"].is_u64());
1313 assert!(parsed["luminance"]["mean"].is_f64());
1314 assert!(parsed["luminance"]["std_dev"].is_f64());
1315 }
1316
1317 #[test]
1318 fn test_histogram_json_struct() {
1319 let data = vec![
1320 100, 100, 100, 255, 100, 100, 100, 255, 200, 200, 200, 255, 200, 200, 200, 255, ];
1325
1326 let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
1327 let hist_json = hist.to_json(2, 2);
1328
1329 assert_eq!(hist_json.width, 2);
1331 assert_eq!(hist_json.height, 2);
1332 assert_eq!(hist_json.total_pixels, 4);
1333
1334 assert_eq!(hist_json.red.bins.len(), 256);
1336 assert_eq!(hist_json.green.bins.len(), 256);
1337 assert_eq!(hist_json.blue.bins.len(), 256);
1338 assert_eq!(hist_json.luminance.bins.len(), 256);
1339
1340 assert_eq!(hist_json.red.count, 4);
1342 assert_eq!(hist_json.green.count, 4);
1343 assert_eq!(hist_json.blue.count, 4);
1344 assert_eq!(hist_json.luminance.count, 4);
1345
1346 assert_eq!(hist_json.red.bins[100], 2);
1348 assert_eq!(hist_json.red.bins[200], 2);
1349 }
1350
1351 #[test]
1352 fn test_histogram_std_dev() {
1353 let data = vec![
1355 128, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255,
1357 ];
1358
1359 let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
1360 let std_dev = hist.std_dev();
1361
1362 assert!(
1364 std_dev.abs() < f64::EPSILON,
1365 "Uniform values should have zero std_dev"
1366 );
1367
1368 let varied_data = vec![
1370 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, ];
1375
1376 let varied_hist =
1377 Histogram::from_rgba(&varied_data, 2, 2).expect("Histogram computation failed");
1378 let varied_std_dev = varied_hist.std_dev();
1379
1380 assert!(
1382 varied_std_dev > 100.0,
1383 "High variation should have high std_dev"
1384 );
1385 }
1386
1387 #[test]
1388 fn test_channel_histogram_statistics() {
1389 let data = vec![
1390 0, 0, 0, 255, 64, 64, 64, 255, 192, 192, 192, 255, 255, 255, 255, 255, ];
1395
1396 let hist = Histogram::from_rgba(&data, 2, 2).expect("Histogram computation failed");
1397 let hist_json = hist.to_json(2, 2);
1398
1399 assert_eq!(hist_json.luminance.min, 0);
1401 assert_eq!(hist_json.luminance.max, 255);
1402
1403 assert!(
1405 (hist_json.luminance.mean - 127.75).abs() < 1.0,
1406 "Mean should be approximately 127.75, got {}",
1407 hist_json.luminance.mean
1408 );
1409 }
1410
1411 #[test]
1412 fn test_custom_bin_histogram() {
1413 let data = vec![
1414 25, 25, 25, 255, 75, 75, 75, 255, 125, 125, 125, 255, 175, 175, 175, 255, ];
1419
1420 let bin_edges = vec![0.0, 50.0, 100.0, 150.0, 200.0, 256.0];
1421 let custom_hist = Histogram::from_rgba_with_bins(&data, 2, 2, &bin_edges)
1422 .expect("Custom bin histogram computation failed");
1423
1424 assert_eq!(custom_hist.num_bins, 5);
1425 assert_eq!(custom_hist.bins.len(), 5);
1426
1427 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);
1435 assert_eq!(custom_hist.max, 175.0);
1436 }
1437
1438 #[test]
1439 fn test_histogram_pretty_json() {
1440 let data = vec![128, 128, 128, 255, 128, 128, 128, 255];
1441
1442 let hist = Histogram::from_rgba(&data, 2, 1).expect("Histogram computation failed");
1443 let hist_json = hist.to_json(2, 1);
1444 let pretty_json = hist_json.to_json_string_pretty();
1445
1446 assert!(
1447 pretty_json.is_ok(),
1448 "Pretty JSON serialization should succeed"
1449 );
1450
1451 let pretty_str = pretty_json.expect("Should have pretty JSON string");
1452 assert!(
1453 pretty_str.contains('\n'),
1454 "Pretty JSON should contain newlines"
1455 );
1456 assert!(
1457 pretty_str.contains(" "),
1458 "Pretty JSON should contain indentation"
1459 );
1460 }
1461
1462 #[test]
1463 fn test_empty_histogram() {
1464 let data = vec![128, 128, 128, 255];
1466
1467 let hist = Histogram::from_rgba(&data, 1, 1).expect("Histogram computation failed");
1468 let hist_json = hist.to_json(1, 1);
1469
1470 assert_eq!(hist_json.total_pixels, 1);
1471 assert_eq!(hist_json.luminance.count, 1);
1472 assert_eq!(hist_json.luminance.min, 128);
1473 assert_eq!(hist_json.luminance.max, 128);
1474 assert!(
1475 hist_json.luminance.std_dev.abs() < f64::EPSILON,
1476 "Single pixel should have zero std_dev"
1477 );
1478 }
1479}