Skip to main content

resamplescope/
reference.rs

1use imgref::{ImgRef, ImgVec};
2
3use crate::filters::KnownFilter;
4
5/// A single weight entry: which source pixel contributes and by how much.
6#[derive(Debug, Clone)]
7pub struct WeightEntry {
8    pub src_pixel: usize,
9    pub weight: f64,
10}
11
12/// The computed weights for a single output pixel.
13#[derive(Debug, Clone)]
14pub struct PixelWeights {
15    pub entries: Vec<WeightEntry>,
16}
17
18/// Compute the exact pixel weight table for a 1D resize operation.
19///
20/// For each output pixel, returns the list of source pixels and their
21/// normalized weights. Uses clamp edge handling (repeats the edge pixel
22/// for out-of-bounds accesses).
23pub 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        // Center of this output pixel in source coordinates.
32        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            // Clamp to valid range.
42            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                // Merge with existing entry for same clamped pixel.
48                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        // Normalize so weights sum to 1.
64        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
76/// Apply 1D weights to a row of source pixels, producing one output row.
77fn 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
91/// Generate the mathematically perfect resize output for a given filter.
92///
93/// Uses separable 2D resize: horizontal pass then vertical pass.
94/// Edge handling is clamp (repeat edge pixel).
95pub 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    // Horizontal pass: resize each row.
104    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    // Vertical pass (only if height changes).
112    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        // Extract the column from temp.
121        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        // Resizing a uniform image should produce a uniform image.
174        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        // Middle pixel should have the peak value.
188        let mid = dst.buf()[(15 / 2) * 555 + 555 / 2];
189        assert!(mid > 200, "peak should be bright, got {mid}");
190    }
191}