1#![forbid(unsafe_code)]
2
3pub 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
53pub type ResizeFn = dyn Fn(ImgRef<'_, u8>, usize, usize) -> ImgVec<u8>;
56
57#[derive(Debug, Clone)]
59pub struct AnalysisConfig {
60 pub srgb: bool,
62 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#[derive(Debug, Clone)]
77pub struct AnalysisResult {
78 pub downscale_curve: Option<FilterCurve>,
80 pub upscale_curve: Option<FilterCurve>,
82 pub scores: Vec<FilterScore>,
84 pub edge_mode: Option<EdgeMode>,
86}
87
88impl AnalysisResult {
89 pub fn best_match(&self) -> Option<&FilterScore> {
91 self.scores.first().filter(|s| s.correlation > 0.99)
92 }
93
94 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 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#[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
141pub fn analyze(resize: &ResizeFn, config: &AnalysisConfig) -> Result<AnalysisResult, Error> {
144 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 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 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 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
185pub 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
210pub 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
241pub use pattern::{generate_dot_pattern, generate_line_pattern};
243
244pub use reference::{compute_weights, perfect_resize};
246
247pub use score::ssim;