Skip to main content

resamplescope/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Reverse-engineer the resampling filter used by any image resizer.
4//!
5//! Port of [ResampleScope](http://entropymine.com/resamplescope/) by Jason Summers
6//! (~1400 lines of C, GPL-3.0-or-later). The original tool generates test PNG images,
7//! has the resizer process them externally, then reconstructs the filter kernel shape
8//! from the output.
9//!
10//! This port replaces that file-based workflow with a **callback API**: you provide a
11//! resize closure, and the crate handles everything in-memory. It adds scoring against
12//! reference filters, SSIM comparison against perfect reference weight tables, and edge
13//! handling detection.
14//!
15//! ## License
16//!
17//! AGPL-3.0-or-later. See [`LICENSE`](https://github.com/imazen/resamplescope-rs/blob/main/LICENSE).
18//!
19//! ## Original work
20//!
21//! - **Author**: Jason Summers
22//! - **Website**: <http://entropymine.com/resamplescope/>
23//! - **License**: GPL-3.0-or-later
24//!
25//! This crate is a derivative work. The test pattern generation and filter
26//! reconstruction algorithms are ported from the original C source.
27//!
28//! ## Filter implementations
29//!
30//! The reference filter math in [`filters`] and the separable 2D resize in
31//! [`reference`] use standard mathematical definitions (sinc, Mitchell-Netravali,
32//! etc.) and standard resampling algorithms. The authoritative, optimized
33//! implementations of these filters live in
34//! [imageflow](https://github.com/imazen/imageflow) by Imazen.
35
36pub mod analyze;
37pub mod edge;
38pub mod filters;
39pub mod graph;
40pub mod pattern;
41pub mod reference;
42pub mod score;
43
44use imgref::{ImgRef, ImgVec};
45use rgb::RGB8;
46
47pub use analyze::FilterCurve;
48pub use edge::EdgeMode;
49pub use filters::KnownFilter;
50pub use reference::{PixelWeights, WeightEntry};
51pub use score::FilterScore;
52
53/// The resize callback type: takes a grayscale source image and target dimensions,
54/// returns the resized grayscale image.
55pub type ResizeFn = dyn Fn(ImgRef<'_, u8>, usize, usize) -> ImgVec<u8>;
56
57/// Configuration for analysis.
58#[derive(Debug, Clone)]
59pub struct AnalysisConfig {
60    /// Whether the resizer operates in sRGB colorspace (converts to linear before resize).
61    pub srgb: bool,
62    /// Whether to detect edge handling mode.
63    pub detect_edges: bool,
64}
65
66impl Default for AnalysisConfig {
67    fn default() -> Self {
68        Self {
69            srgb: false,
70            detect_edges: true,
71        }
72    }
73}
74
75/// Complete analysis result from probing a resizer.
76#[derive(Debug, Clone)]
77pub struct AnalysisResult {
78    /// Reconstructed filter from dot pattern (downscale, 557->555).
79    pub downscale_curve: Option<FilterCurve>,
80    /// Reconstructed filter from line pattern (upscale, 15->555).
81    pub upscale_curve: Option<FilterCurve>,
82    /// Scores against known reference filters, sorted best-first by correlation.
83    pub scores: Vec<FilterScore>,
84    /// Detected edge handling mode, if requested.
85    pub edge_mode: Option<EdgeMode>,
86}
87
88impl AnalysisResult {
89    /// Returns the best-matching filter if correlation exceeds 0.99.
90    pub fn best_match(&self) -> Option<&FilterScore> {
91        self.scores.first().filter(|s| s.correlation > 0.99)
92    }
93
94    /// Render a scope graph showing the reconstructed filter curve(s).
95    pub fn render_graph(&self) -> ImgVec<RGB8> {
96        graph::render(
97            self.downscale_curve.as_ref(),
98            self.upscale_curve.as_ref(),
99            None,
100        )
101    }
102
103    /// Render a scope graph with a reference filter overlay.
104    pub fn render_graph_with_reference(&self, filter: KnownFilter) -> ImgVec<RGB8> {
105        graph::render(
106            self.downscale_curve.as_ref(),
107            self.upscale_curve.as_ref(),
108            Some(filter),
109        )
110    }
111}
112
113/// Error type for analysis operations.
114#[derive(Debug, thiserror::Error)]
115pub enum Error {
116    #[error(
117        "resize callback returned wrong dimensions: expected {expected_w}x{expected_h}, got {actual_w}x{actual_h}"
118    )]
119    WrongDimensions {
120        expected_w: usize,
121        expected_h: usize,
122        actual_w: usize,
123        actual_h: usize,
124    },
125    #[error("analysis produced no usable data")]
126    NoData,
127}
128
129fn check_dimensions(img: &ImgVec<u8>, expected_w: usize, expected_h: usize) -> Result<(), Error> {
130    if img.width() != expected_w || img.height() != expected_h {
131        return Err(Error::WrongDimensions {
132            expected_w,
133            expected_h,
134            actual_w: img.width(),
135            actual_h: img.height(),
136        });
137    }
138    Ok(())
139}
140
141/// Run both downscale and upscale analysis, score against known filters,
142/// and optionally detect edge handling.
143pub fn analyze(resize: &ResizeFn, config: &AnalysisConfig) -> Result<AnalysisResult, Error> {
144    // Downscale analysis (dot pattern).
145    let dot_src = pattern::generate_dot_pattern();
146    let (dot_w, dot_h) = analyze::dot_target();
147    let dot_resized = resize(dot_src.as_ref(), dot_w, dot_h);
148    check_dimensions(&dot_resized, dot_w, dot_h)?;
149    let downscale_curve = analyze::analyze_dot(&dot_resized.as_ref(), config.srgb);
150
151    // Upscale analysis (line pattern).
152    let line_src = pattern::generate_line_pattern();
153    let (line_w, line_h) = analyze::line_target();
154    let line_resized = resize(line_src.as_ref(), line_w, line_h);
155    check_dimensions(&line_resized, line_w, line_h)?;
156    let upscale_curve = analyze::analyze_line(&line_resized.as_ref(), config.srgb);
157
158    // Score using the upscale curve (higher resolution, cleaner data).
159    // Fall back to downscale if upscale has no points.
160    let scoring_curve = if !upscale_curve.points.is_empty() {
161        &upscale_curve
162    } else if !downscale_curve.points.is_empty() {
163        &downscale_curve
164    } else {
165        return Err(Error::NoData);
166    };
167
168    let scores = score::score_against_all(scoring_curve);
169
170    // Edge detection.
171    let edge_mode = if config.detect_edges {
172        Some(edge::detect(resize))
173    } else {
174        None
175    };
176
177    Ok(AnalysisResult {
178        downscale_curve: Some(downscale_curve),
179        upscale_curve: Some(upscale_curve),
180        scores,
181        edge_mode,
182    })
183}
184
185/// Run only the downscale analysis (dot pattern, 557->555).
186pub fn analyze_downscale(
187    resize: &ResizeFn,
188    config: &AnalysisConfig,
189) -> Result<AnalysisResult, Error> {
190    let dot_src = pattern::generate_dot_pattern();
191    let (dot_w, dot_h) = analyze::dot_target();
192    let dot_resized = resize(dot_src.as_ref(), dot_w, dot_h);
193    check_dimensions(&dot_resized, dot_w, dot_h)?;
194    let downscale_curve = analyze::analyze_dot(&dot_resized.as_ref(), config.srgb);
195
196    if downscale_curve.points.is_empty() {
197        return Err(Error::NoData);
198    }
199
200    let scores = score::score_against_all(&downscale_curve);
201
202    Ok(AnalysisResult {
203        downscale_curve: Some(downscale_curve),
204        upscale_curve: None,
205        scores,
206        edge_mode: None,
207    })
208}
209
210/// Run only the upscale analysis (line pattern, 15->555).
211pub fn analyze_upscale(
212    resize: &ResizeFn,
213    config: &AnalysisConfig,
214) -> Result<AnalysisResult, Error> {
215    let line_src = pattern::generate_line_pattern();
216    let (line_w, line_h) = analyze::line_target();
217    let line_resized = resize(line_src.as_ref(), line_w, line_h);
218    check_dimensions(&line_resized, line_w, line_h)?;
219    let upscale_curve = analyze::analyze_line(&line_resized.as_ref(), config.srgb);
220
221    if upscale_curve.points.is_empty() {
222        return Err(Error::NoData);
223    }
224
225    let scores = score::score_against_all(&upscale_curve);
226
227    let edge_mode = if config.detect_edges {
228        Some(edge::detect(resize))
229    } else {
230        None
231    };
232
233    Ok(AnalysisResult {
234        downscale_curve: None,
235        upscale_curve: Some(upscale_curve),
236        scores,
237        edge_mode,
238    })
239}
240
241// Re-export convenience functions from pattern.
242pub use pattern::{generate_dot_pattern, generate_line_pattern};
243
244// Re-export reference resize functions.
245pub use reference::{compute_weights, perfect_resize};
246
247// Re-export SSIM.
248pub use score::ssim;