resamplescope/
reference.rs1use imgref::{ImgRef, ImgVec};
2
3use crate::filters::KnownFilter;
4
5#[derive(Debug, Clone)]
7pub struct WeightEntry {
8 pub src_pixel: usize,
9 pub weight: f64,
10}
11
12#[derive(Debug, Clone)]
14pub struct PixelWeights {
15 pub entries: Vec<WeightEntry>,
16}
17
18pub fn compute_weights(filter: KnownFilter, src_size: usize, dst_size: usize) -> Vec<PixelWeights> {
24 let scale = dst_size as f64 / src_size as f64;
25 let filter_scale = if scale < 1.0 { 1.0 / scale } else { 1.0 };
26 let support = filter.support() * filter_scale;
27
28 let mut result = Vec::with_capacity(dst_size);
29
30 for dst_x in 0..dst_size {
31 let center = (dst_x as f64 + 0.5) / scale - 0.5;
33
34 let left = (center - support).ceil() as isize;
35 let right = (center + support).floor() as isize;
36
37 let mut entries = Vec::new();
38 let mut total = 0.0;
39
40 for src_x in left..=right {
41 let clamped = src_x.clamp(0, src_size as isize - 1) as usize;
43 let distance = (src_x as f64 - center) / filter_scale;
44 let w = filter.evaluate(distance);
45
46 if w.abs() > 1e-12 {
47 if let Some(existing) = entries
49 .iter_mut()
50 .find(|e: &&mut WeightEntry| e.src_pixel == clamped)
51 {
52 existing.weight += w;
53 } else {
54 entries.push(WeightEntry {
55 src_pixel: clamped,
56 weight: w,
57 });
58 }
59 total += w;
60 }
61 }
62
63 if total.abs() > 1e-12 {
65 for e in &mut entries {
66 e.weight /= total;
67 }
68 }
69
70 result.push(PixelWeights { entries });
71 }
72
73 result
74}
75
76fn apply_weights_row(weights: &[PixelWeights], src_row: &[u8]) -> Vec<u8> {
78 weights
79 .iter()
80 .map(|pw| {
81 let val: f64 = pw
82 .entries
83 .iter()
84 .map(|e| src_row[e.src_pixel] as f64 * e.weight)
85 .sum();
86 val.round().clamp(0.0, 255.0) as u8
87 })
88 .collect()
89}
90
91pub fn perfect_resize(
96 src: ImgRef<'_, u8>,
97 dst_width: usize,
98 dst_height: usize,
99 filter: KnownFilter,
100) -> ImgVec<u8> {
101 let h_weights = compute_weights(filter, src.width(), dst_width);
102
103 let mut temp = vec![0u8; dst_width * src.height()];
105 for y in 0..src.height() {
106 let src_row = &src.buf()[y * src.stride()..][..src.width()];
107 let dst_row = apply_weights_row(&h_weights, src_row);
108 temp[y * dst_width..][..dst_width].copy_from_slice(&dst_row);
109 }
110
111 if dst_height == src.height() {
113 return ImgVec::new(temp, dst_width, dst_height);
114 }
115
116 let v_weights = compute_weights(filter, src.height(), dst_height);
117 let mut result = vec![0u8; dst_width * dst_height];
118
119 for x in 0..dst_width {
120 let col: Vec<u8> = (0..src.height()).map(|y| temp[y * dst_width + x]).collect();
122
123 for (y, pw) in v_weights.iter().enumerate() {
124 let val: f64 = pw
125 .entries
126 .iter()
127 .map(|e| col[e.src_pixel] as f64 * e.weight)
128 .sum();
129 result[y * dst_width + x] = val.round().clamp(0.0, 255.0) as u8;
130 }
131 }
132
133 ImgVec::new(result, dst_width, dst_height)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::pattern;
140
141 #[test]
142 fn weights_sum_to_one() {
143 for filter in crate::filters::KnownFilter::all_named() {
144 let weights = compute_weights(*filter, 15, 555);
145 for (i, pw) in weights.iter().enumerate() {
146 let sum: f64 = pw.entries.iter().map(|e| e.weight).sum();
147 assert!(
148 (sum - 1.0).abs() < 1e-6,
149 "{}: pixel {i} weights sum to {sum}",
150 filter.name()
151 );
152 }
153 }
154 }
155
156 #[test]
157 fn weights_sum_to_one_downscale() {
158 for filter in crate::filters::KnownFilter::all_named() {
159 let weights = compute_weights(*filter, 557, 555);
160 for (i, pw) in weights.iter().enumerate() {
161 let sum: f64 = pw.entries.iter().map(|e| e.weight).sum();
162 assert!(
163 (sum - 1.0).abs() < 1e-6,
164 "{}: pixel {i} weights sum to {sum}",
165 filter.name()
166 );
167 }
168 }
169 }
170
171 #[test]
172 fn perfect_resize_preserves_uniform() {
173 let src = ImgVec::new(vec![128u8; 15 * 15], 15, 15);
175 let dst = perfect_resize(src.as_ref(), 555, 15, KnownFilter::Lanczos3);
176 for &v in dst.buf() {
177 assert_eq!(v, 128, "uniform image not preserved");
178 }
179 }
180
181 #[test]
182 fn perfect_resize_line_pattern() {
183 let src = pattern::generate_line_pattern();
184 let dst = perfect_resize(src.as_ref(), 555, 15, KnownFilter::Lanczos3);
185 assert_eq!(dst.width(), 555);
186 assert_eq!(dst.height(), 15);
187 let mid = dst.buf()[(15 / 2) * 555 + 555 / 2];
189 assert!(mid > 200, "peak should be bright, got {mid}");
190 }
191}