1#![forbid(unsafe_code)]
7#![allow(clippy::cast_lossless)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::cast_possible_truncation)]
10#![allow(clippy::cast_sign_loss)]
11#![allow(clippy::cast_possible_wrap)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::many_single_char_names)]
14#![allow(clippy::missing_errors_doc)]
15#![allow(clippy::match_same_arms)]
16#![allow(clippy::doc_markdown)]
17#![allow(clippy::unused_self)]
18#![allow(clippy::unnecessary_cast)]
19#![allow(clippy::bool_to_int_with_if)]
20#![allow(clippy::needless_range_loop)]
21#![allow(clippy::too_many_lines)]
22#![allow(clippy::unnecessary_wraps)]
23#![allow(clippy::map_unwrap_or)]
24#![allow(clippy::no_effect_underscore_binding)]
25#![allow(clippy::unreadable_literal)]
26#![allow(dead_code)]
27
28use std::f64::consts::PI;
29
30use crate::error::{GraphError, GraphResult};
31use crate::frame::FilterFrame;
32use crate::node::{Node, NodeId, NodeState, NodeType};
33use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
34use oximedia_codec::{Plane, VideoFrame};
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
38pub enum ScaleAlgorithm {
39 Nearest,
41 Bilinear,
43 #[default]
45 Bicubic,
46 CatmullRom,
48 Lanczos2,
50 Lanczos3,
52 Lanczos4,
54}
55
56impl ScaleAlgorithm {
57 #[must_use]
59 pub fn support(&self) -> f64 {
60 match self {
61 Self::Nearest => 0.5,
62 Self::Bilinear => 1.0,
63 Self::Bicubic | Self::CatmullRom => 2.0,
64 Self::Lanczos2 => 2.0,
65 Self::Lanczos3 => 3.0,
66 Self::Lanczos4 => 4.0,
67 }
68 }
69
70 #[must_use]
72 pub fn kernel(&self, x: f64) -> f64 {
73 let x = x.abs();
74 match self {
75 Self::Nearest => {
76 if x < 0.5 {
77 1.0
78 } else {
79 0.0
80 }
81 }
82 Self::Bilinear => bilinear_kernel(x),
83 Self::Bicubic => mitchell_netravali_kernel(x),
84 Self::CatmullRom => catmull_rom_kernel(x),
85 Self::Lanczos2 => lanczos_kernel(x, 2.0),
86 Self::Lanczos3 => lanczos_kernel(x, 3.0),
87 Self::Lanczos4 => lanczos_kernel(x, 4.0),
88 }
89 }
90}
91
92fn bilinear_kernel(x: f64) -> f64 {
94 if x < 1.0 {
95 1.0 - x
96 } else {
97 0.0
98 }
99}
100
101fn mitchell_netravali_kernel(x: f64) -> f64 {
103 const B: f64 = 1.0 / 3.0;
104 const C: f64 = 1.0 / 3.0;
105
106 let x2 = x * x;
107 let x3 = x2 * x;
108
109 if x < 1.0 {
110 ((12.0 - 9.0 * B - 6.0 * C) * x3 + (-18.0 + 12.0 * B + 6.0 * C) * x2 + (6.0 - 2.0 * B))
111 / 6.0
112 } else if x < 2.0 {
113 ((-B - 6.0 * C) * x3
114 + (6.0 * B + 30.0 * C) * x2
115 + (-12.0 * B - 48.0 * C) * x
116 + (8.0 * B + 24.0 * C))
117 / 6.0
118 } else {
119 0.0
120 }
121}
122
123fn catmull_rom_kernel(x: f64) -> f64 {
125 let x2 = x * x;
126 let x3 = x2 * x;
127
128 if x < 1.0 {
129 1.5 * x3 - 2.5 * x2 + 1.0
130 } else if x < 2.0 {
131 -0.5 * x3 + 2.5 * x2 - 4.0 * x + 2.0
132 } else {
133 0.0
134 }
135}
136
137fn lanczos_kernel(x: f64, a: f64) -> f64 {
139 if x == 0.0 {
140 1.0
141 } else if x < a {
142 sinc(x) * sinc(x / a)
143 } else {
144 0.0
145 }
146}
147
148fn sinc(x: f64) -> f64 {
150 if x == 0.0 {
151 1.0
152 } else {
153 let pix = PI * x;
154 pix.sin() / pix
155 }
156}
157
158#[derive(Clone, Debug)]
160pub struct ScaleConfig {
161 pub width: u32,
163 pub height: u32,
165 pub algorithm: ScaleAlgorithm,
167 pub antialias: bool,
169 pub preserve_aspect: bool,
171}
172
173impl ScaleConfig {
174 #[must_use]
176 pub fn new(width: u32, height: u32) -> Self {
177 Self {
178 width,
179 height,
180 algorithm: ScaleAlgorithm::default(),
181 antialias: true,
182 preserve_aspect: false,
183 }
184 }
185
186 #[must_use]
188 pub fn with_algorithm(mut self, algorithm: ScaleAlgorithm) -> Self {
189 self.algorithm = algorithm;
190 self
191 }
192
193 #[must_use]
195 pub fn with_antialias(mut self, enabled: bool) -> Self {
196 self.antialias = enabled;
197 self
198 }
199
200 #[must_use]
202 pub fn with_preserve_aspect(mut self, enabled: bool) -> Self {
203 self.preserve_aspect = enabled;
204 self
205 }
206}
207
208pub struct ScaleFilter {
226 id: NodeId,
227 name: String,
228 state: NodeState,
229 inputs: Vec<InputPort>,
230 outputs: Vec<OutputPort>,
231 config: ScaleConfig,
232 h_coefficients: Vec<FilterCoefficients>,
234 v_coefficients: Vec<FilterCoefficients>,
236 cached_src_dims: Option<(u32, u32)>,
238}
239
240#[derive(Clone, Debug)]
242struct FilterCoefficients {
243 start: usize,
245 weights: Vec<f64>,
247}
248
249impl ScaleFilter {
250 #[must_use]
252 pub fn new(id: NodeId, name: impl Into<String>, config: ScaleConfig) -> Self {
253 let output_format =
254 PortFormat::Video(VideoPortFormat::any().with_dimensions(config.width, config.height));
255
256 Self {
257 id,
258 name: name.into(),
259 state: NodeState::Idle,
260 inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
261 .with_format(PortFormat::Video(VideoPortFormat::any()))],
262 outputs: vec![
263 OutputPort::new(PortId(0), "output", PortType::Video).with_format(output_format)
264 ],
265 config,
266 h_coefficients: Vec::new(),
267 v_coefficients: Vec::new(),
268 cached_src_dims: None,
269 }
270 }
271
272 #[must_use]
274 pub fn config(&self) -> &ScaleConfig {
275 &self.config
276 }
277
278 pub fn set_dimensions(&mut self, width: u32, height: u32) {
280 if self.config.width != width || self.config.height != height {
281 self.config.width = width;
282 self.config.height = height;
283 self.cached_src_dims = None;
284 self.h_coefficients.clear();
285 self.v_coefficients.clear();
286 }
287 }
288
289 fn compute_coefficients(&mut self, src_width: u32, src_height: u32) {
291 if self.cached_src_dims == Some((src_width, src_height)) {
292 return;
293 }
294
295 self.h_coefficients =
296 Self::compute_1d_coefficients(src_width, self.config.width, &self.config);
297 self.v_coefficients =
298 Self::compute_1d_coefficients(src_height, self.config.height, &self.config);
299 self.cached_src_dims = Some((src_width, src_height));
300 }
301
302 fn compute_1d_coefficients(
304 src_size: u32,
305 dst_size: u32,
306 config: &ScaleConfig,
307 ) -> Vec<FilterCoefficients> {
308 let mut coefficients = Vec::with_capacity(dst_size as usize);
309 let scale = src_size as f64 / dst_size as f64;
310 let algorithm = config.algorithm;
311
312 let filter_scale = if config.antialias && scale > 1.0 {
314 scale
315 } else {
316 1.0
317 };
318
319 let support = algorithm.support() * filter_scale;
320
321 for dst_pos in 0..dst_size {
322 let center = (dst_pos as f64 + 0.5) * scale - 0.5;
323 let start = ((center - support).floor() as i64).max(0) as usize;
324 let end = ((center + support).ceil() as i64).min(src_size as i64) as usize;
325
326 let mut weights = Vec::with_capacity(end - start);
327 let mut sum = 0.0;
328
329 for src_pos in start..end {
330 let distance = (src_pos as f64 - center) / filter_scale;
331 let weight = algorithm.kernel(distance);
332 weights.push(weight);
333 sum += weight;
334 }
335
336 if sum != 0.0 {
338 for w in &mut weights {
339 *w /= sum;
340 }
341 }
342
343 coefficients.push(FilterCoefficients { start, weights });
344 }
345
346 coefficients
347 }
348
349 #[allow(clippy::too_many_arguments)]
351 fn scale_plane(
352 &self,
353 src: &Plane,
354 src_width: u32,
355 src_height: u32,
356 dst_width: u32,
357 dst_height: u32,
358 ) -> Plane {
359 let mut intermediate = vec![0.0f64; dst_width as usize * src_height as usize];
361
362 for y in 0..src_height as usize {
364 let src_row = src.row(y);
365 for (x, coef) in self.h_coefficients.iter().enumerate() {
366 let mut sum = 0.0;
367 for (i, &weight) in coef.weights.iter().enumerate() {
368 let src_x = (coef.start + i).min(src_width as usize - 1);
369 sum += src_row.get(src_x).copied().unwrap_or(0) as f64 * weight;
370 }
371 intermediate[y * dst_width as usize + x] = sum;
372 }
373 }
374
375 let mut dst_data = vec![0u8; dst_width as usize * dst_height as usize];
377
378 for y in 0..dst_height as usize {
379 let coef = &self.v_coefficients[y];
380 for x in 0..dst_width as usize {
381 let mut sum = 0.0;
382 for (i, &weight) in coef.weights.iter().enumerate() {
383 let src_y = (coef.start + i).min(src_height as usize - 1);
384 sum += intermediate[src_y * dst_width as usize + x] * weight;
385 }
386 dst_data[y * dst_width as usize + x] = sum.round().clamp(0.0, 255.0) as u8;
387 }
388 }
389
390 Plane::new(dst_data, dst_width as usize)
391 }
392
393 fn scale_frame(&mut self, input: &VideoFrame) -> VideoFrame {
395 self.compute_coefficients(input.width, input.height);
396
397 let mut output = VideoFrame::new(input.format, self.config.width, self.config.height);
398 output.timestamp = input.timestamp;
399 output.frame_type = input.frame_type;
400 output.color_info = input.color_info;
401
402 for (i, src_plane) in input.planes.iter().enumerate() {
404 let (src_w, src_h) = input.plane_dimensions(i);
405 let (dst_w, dst_h) = output.plane_dimensions(i);
406
407 if i > 0 && input.format.is_yuv() {
409 let old_h = self.h_coefficients.clone();
410 let old_v = self.v_coefficients.clone();
411 let old_cached = self.cached_src_dims;
412
413 self.h_coefficients = Self::compute_1d_coefficients(src_w, dst_w, &self.config);
414 self.v_coefficients = Self::compute_1d_coefficients(src_h, dst_h, &self.config);
415
416 let plane = self.scale_plane(src_plane, src_w, src_h, dst_w, dst_h);
417 output.planes.push(plane);
418
419 self.h_coefficients = old_h;
420 self.v_coefficients = old_v;
421 self.cached_src_dims = old_cached;
422 } else {
423 let plane = self.scale_plane(src_plane, src_w, src_h, dst_w, dst_h);
424 output.planes.push(plane);
425 }
426 }
427
428 output
429 }
430}
431
432impl Node for ScaleFilter {
433 fn id(&self) -> NodeId {
434 self.id
435 }
436
437 fn name(&self) -> &str {
438 &self.name
439 }
440
441 fn node_type(&self) -> NodeType {
442 NodeType::Filter
443 }
444
445 fn state(&self) -> NodeState {
446 self.state
447 }
448
449 fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
450 if !self.state.can_transition_to(state) {
451 return Err(GraphError::InvalidStateTransition {
452 node: self.id,
453 from: self.state.to_string(),
454 to: state.to_string(),
455 });
456 }
457 self.state = state;
458 Ok(())
459 }
460
461 fn inputs(&self) -> &[InputPort] {
462 &self.inputs
463 }
464
465 fn outputs(&self) -> &[OutputPort] {
466 &self.outputs
467 }
468
469 fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
470 match input {
471 Some(FilterFrame::Video(frame)) => {
472 let scaled = self.scale_frame(&frame);
473 Ok(Some(FilterFrame::Video(scaled)))
474 }
475 Some(_) => Err(GraphError::PortTypeMismatch {
476 expected: "Video".to_string(),
477 actual: "Audio".to_string(),
478 }),
479 None => Ok(None),
480 }
481 }
482}
483
484#[derive(Debug)]
486pub struct NearestNeighborScaler {
487 dst_width: u32,
488 dst_height: u32,
489}
490
491impl NearestNeighborScaler {
492 #[must_use]
494 pub fn new(dst_width: u32, dst_height: u32) -> Self {
495 Self {
496 dst_width,
497 dst_height,
498 }
499 }
500
501 #[must_use]
503 pub fn scale_plane(&self, src: &Plane, src_width: u32, src_height: u32) -> Plane {
504 let mut dst_data = vec![0u8; self.dst_width as usize * self.dst_height as usize];
505
506 let x_ratio = src_width as f64 / self.dst_width as f64;
507 let y_ratio = src_height as f64 / self.dst_height as f64;
508
509 for y in 0..self.dst_height as usize {
510 let src_y = ((y as f64 + 0.5) * y_ratio).floor() as usize;
511 let src_y = src_y.min(src_height as usize - 1);
512 let src_row = src.row(src_y);
513
514 for x in 0..self.dst_width as usize {
515 let src_x = ((x as f64 + 0.5) * x_ratio).floor() as usize;
516 let src_x = src_x.min(src_width as usize - 1);
517 dst_data[y * self.dst_width as usize + x] =
518 src_row.get(src_x).copied().unwrap_or(0);
519 }
520 }
521
522 Plane::new(dst_data, self.dst_width as usize)
523 }
524}
525
526#[derive(Debug)]
528pub struct BilinearScaler {
529 dst_width: u32,
530 dst_height: u32,
531}
532
533impl BilinearScaler {
534 #[must_use]
536 pub fn new(dst_width: u32, dst_height: u32) -> Self {
537 Self {
538 dst_width,
539 dst_height,
540 }
541 }
542
543 #[must_use]
545 pub fn scale_plane(&self, src: &Plane, src_width: u32, src_height: u32) -> Plane {
546 let mut dst_data = vec![0u8; self.dst_width as usize * self.dst_height as usize];
547
548 let x_ratio = (src_width as f64 - 1.0) / (self.dst_width as f64 - 1.0).max(1.0);
549 let y_ratio = (src_height as f64 - 1.0) / (self.dst_height as f64 - 1.0).max(1.0);
550
551 for y in 0..self.dst_height as usize {
552 let src_y = y as f64 * y_ratio;
553 let y0 = src_y.floor() as usize;
554 let y1 = (y0 + 1).min(src_height as usize - 1);
555 let y_frac = src_y - y0 as f64;
556
557 let row0 = src.row(y0);
558 let row1 = src.row(y1);
559
560 for x in 0..self.dst_width as usize {
561 let src_x = x as f64 * x_ratio;
562 let x0 = src_x.floor() as usize;
563 let x1 = (x0 + 1).min(src_width as usize - 1);
564 let x_frac = src_x - x0 as f64;
565
566 let p00 = row0.get(x0).copied().unwrap_or(0) as f64;
567 let p10 = row0.get(x1).copied().unwrap_or(0) as f64;
568 let p01 = row1.get(x0).copied().unwrap_or(0) as f64;
569 let p11 = row1.get(x1).copied().unwrap_or(0) as f64;
570
571 let top = p00 * (1.0 - x_frac) + p10 * x_frac;
572 let bottom = p01 * (1.0 - x_frac) + p11 * x_frac;
573 let value = top * (1.0 - y_frac) + bottom * y_frac;
574
575 dst_data[y * self.dst_width as usize + x] = value.round().clamp(0.0, 255.0) as u8;
576 }
577 }
578
579 Plane::new(dst_data, self.dst_width as usize)
580 }
581}
582
583#[must_use]
585pub fn calculate_aspect_fit(
586 src_width: u32,
587 src_height: u32,
588 dst_width: u32,
589 dst_height: u32,
590) -> (u32, u32) {
591 let src_aspect = src_width as f64 / src_height as f64;
592 let dst_aspect = dst_width as f64 / dst_height as f64;
593
594 if src_aspect > dst_aspect {
595 let new_height = (dst_width as f64 / src_aspect).round() as u32;
597 (dst_width, new_height)
598 } else {
599 let new_width = (dst_height as f64 * src_aspect).round() as u32;
601 (new_width, dst_height)
602 }
603}
604
605#[must_use]
607pub fn calculate_aspect_fill(
608 src_width: u32,
609 src_height: u32,
610 dst_width: u32,
611 dst_height: u32,
612) -> (u32, u32) {
613 let src_aspect = src_width as f64 / src_height as f64;
614 let dst_aspect = dst_width as f64 / dst_height as f64;
615
616 if src_aspect < dst_aspect {
617 let new_height = (dst_width as f64 / src_aspect).round() as u32;
619 (dst_width, new_height)
620 } else {
621 let new_width = (dst_height as f64 * src_aspect).round() as u32;
623 (new_width, dst_height)
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use oximedia_core::PixelFormat;
631
632 fn create_test_frame(width: u32, height: u32) -> VideoFrame {
633 let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
634 frame.allocate();
635
636 if let Some(plane) = frame.planes.get_mut(0) {
638 let mut data = vec![0u8; width as usize * height as usize];
639 for y in 0..height as usize {
640 for x in 0..width as usize {
641 data[y * width as usize + x] = ((x + y) % 256) as u8;
642 }
643 }
644 *plane = Plane::new(data, width as usize);
645 }
646
647 frame
648 }
649
650 #[test]
651 fn test_scale_filter_creation() {
652 let config = ScaleConfig::new(1280, 720)
653 .with_algorithm(ScaleAlgorithm::Lanczos3)
654 .with_antialias(true);
655
656 let filter = ScaleFilter::new(NodeId(0), "scale", config);
657
658 assert_eq!(filter.id(), NodeId(0));
659 assert_eq!(filter.name(), "scale");
660 assert_eq!(filter.config().width, 1280);
661 assert_eq!(filter.config().height, 720);
662 assert_eq!(filter.config().algorithm, ScaleAlgorithm::Lanczos3);
663 }
664
665 #[test]
666 fn test_scale_algorithms() {
667 assert_eq!(ScaleAlgorithm::Nearest.support(), 0.5);
668 assert_eq!(ScaleAlgorithm::Bilinear.support(), 1.0);
669 assert_eq!(ScaleAlgorithm::Bicubic.support(), 2.0);
670 assert_eq!(ScaleAlgorithm::Lanczos2.support(), 2.0);
671 assert_eq!(ScaleAlgorithm::Lanczos3.support(), 3.0);
672 assert_eq!(ScaleAlgorithm::Lanczos4.support(), 4.0);
673 }
674
675 #[test]
676 fn test_kernel_values() {
677 assert!((ScaleAlgorithm::Nearest.kernel(0.0) - 1.0).abs() < 0.001);
679 assert!((ScaleAlgorithm::Nearest.kernel(0.6) - 0.0).abs() < 0.001);
680
681 assert!((ScaleAlgorithm::Bilinear.kernel(0.0) - 1.0).abs() < 0.001);
683 assert!((ScaleAlgorithm::Bilinear.kernel(0.5) - 0.5).abs() < 0.001);
684 assert!((ScaleAlgorithm::Bilinear.kernel(1.0) - 0.0).abs() < 0.001);
685
686 assert!((ScaleAlgorithm::Lanczos3.kernel(0.0) - 1.0).abs() < 0.001);
688 }
689
690 #[test]
691 fn test_scale_downscale() {
692 let config = ScaleConfig::new(80, 60);
693 let mut filter = ScaleFilter::new(NodeId(0), "scale", config);
694
695 let input = create_test_frame(160, 120);
696 let result = filter.scale_frame(&input);
697
698 assert_eq!(result.width, 80);
699 assert_eq!(result.height, 60);
700 assert_eq!(result.planes.len(), input.planes.len());
701 }
702
703 #[test]
704 fn test_scale_upscale() {
705 let config = ScaleConfig::new(320, 240);
706 let mut filter = ScaleFilter::new(NodeId(0), "scale", config);
707
708 let input = create_test_frame(160, 120);
709 let result = filter.scale_frame(&input);
710
711 assert_eq!(result.width, 320);
712 assert_eq!(result.height, 240);
713 }
714
715 #[test]
716 fn test_nearest_neighbor_scaler() {
717 let scaler = NearestNeighborScaler::new(320, 240);
718
719 let src_data = vec![128u8; 640 * 480];
720 let src_plane = Plane::new(src_data, 640);
721
722 let result = scaler.scale_plane(&src_plane, 640, 480);
723 assert_eq!(result.stride, 320);
724 }
725
726 #[test]
727 fn test_bilinear_scaler() {
728 let scaler = BilinearScaler::new(320, 240);
729
730 let src_data = vec![128u8; 640 * 480];
731 let src_plane = Plane::new(src_data, 640);
732
733 let result = scaler.scale_plane(&src_plane, 640, 480);
734 assert_eq!(result.stride, 320);
735 }
736
737 #[test]
738 fn test_aspect_fit() {
739 let (w, h) = calculate_aspect_fit(1920, 1080, 640, 480);
741 assert_eq!(w, 640);
742 assert!(h <= 480);
743
744 let (w, h) = calculate_aspect_fit(640, 480, 1920, 1080);
746 assert!(w <= 1920);
747 assert_eq!(h, 1080);
748 }
749
750 #[test]
751 fn test_aspect_fill() {
752 let (w, h) = calculate_aspect_fill(1920, 1080, 640, 480);
754 assert!(w >= 640 || h >= 480);
755 }
756
757 #[test]
758 fn test_sinc_function() {
759 assert!((sinc(0.0) - 1.0).abs() < 0.001);
760 assert!(sinc(1.0).abs() < 0.001);
762 }
763
764 #[test]
765 fn test_node_trait_implementation() {
766 let config = ScaleConfig::new(1280, 720);
767 let mut filter = ScaleFilter::new(NodeId(42), "test_scale", config);
768
769 assert_eq!(filter.node_type(), NodeType::Filter);
770 assert_eq!(filter.state(), NodeState::Idle);
771 assert_eq!(filter.inputs().len(), 1);
772 assert_eq!(filter.outputs().len(), 1);
773
774 filter
775 .set_state(NodeState::Processing)
776 .expect("set_state should succeed");
777 assert_eq!(filter.state(), NodeState::Processing);
778 }
779
780 #[test]
781 fn test_process_none_input() {
782 let config = ScaleConfig::new(1280, 720);
783 let mut filter = ScaleFilter::new(NodeId(0), "scale", config);
784
785 let result = filter.process(None).expect("process should succeed");
786 assert!(result.is_none());
787 }
788
789 #[test]
790 fn test_scale_config_builder() {
791 let config = ScaleConfig::new(1920, 1080)
792 .with_algorithm(ScaleAlgorithm::CatmullRom)
793 .with_antialias(false)
794 .with_preserve_aspect(true);
795
796 assert_eq!(config.width, 1920);
797 assert_eq!(config.height, 1080);
798 assert_eq!(config.algorithm, ScaleAlgorithm::CatmullRom);
799 assert!(!config.antialias);
800 assert!(config.preserve_aspect);
801 }
802}