1#![allow(
5 clippy::indexing_slicing,
6 reason = "sample indices are bounded by the descriptor's own dimensions"
7)]
8
9use std::sync::Arc;
10
11use otf_pixels_core::{
12 Animation, Background, ChannelLayout, Comment, Density, Format, ImageDescriptor, Orientation,
13 PixelFormat, SampleKind, SourceInfo,
14};
15
16#[derive(Debug, Clone, PartialEq)]
19#[non_exhaustive]
20pub struct SourceMetadata {
21 pub format: Format,
23 pub size: Option<u64>,
25 pub width: u32,
27 pub height: u32,
29 pub pixel: PixelFormat,
31 pub channels: usize,
33 pub has_alpha: bool,
35 pub bits_per_sample: Option<u8>,
38 pub orientation: Orientation,
41 pub pages: u32,
43 pub animation: Option<Animation>,
46 pub density: Option<Density>,
48 pub progressive: bool,
50 pub palette: bool,
52 pub chroma_subsampling: Option<&'static str>,
54 pub background: Option<Background>,
56 pub compression: Option<&'static str>,
58 pub comments: Vec<Comment>,
60 pub icc: Option<Arc<[u8]>>,
62 pub exif: Option<Arc<[u8]>>,
64 pub xmp: Option<Arc<[u8]>>,
66}
67
68impl SourceMetadata {
69 #[allow(
70 clippy::too_many_arguments,
71 reason = "everything a decoder reports, once"
72 )]
73 pub(crate) fn new(
74 format: Format,
75 stored: ImageDescriptor,
76 orientation: Orientation,
77 pages: u32,
78 animation: Option<Animation>,
79 icc: Option<&[u8]>,
80 exif: Option<&[u8]>,
81 xmp: Option<Arc<[u8]>>,
82 info: SourceInfo,
83 ) -> Self {
84 Self {
85 format,
86 size: None,
87 width: stored.width,
88 height: stored.height,
89 pixel: stored.pixel,
90 channels: stored.pixel.channels(),
91 has_alpha: stored.pixel.has_alpha(),
92 bits_per_sample: info.bits_per_sample,
93 orientation,
94 pages,
95 animation,
96 density: info.density,
97 progressive: info.progressive,
98 palette: info.palette,
99 chroma_subsampling: info.chroma_subsampling,
100 background: info.background,
101 compression: info.compression,
102 comments: info.comments,
103 icc: icc.map(Arc::from),
104 exif: exif.map(Arc::from),
105 xmp,
106 }
107 }
108
109 #[must_use]
111 pub const fn has_profile(&self) -> bool {
112 self.icc.is_some()
113 }
114
115 #[must_use]
117 pub const fn oriented_width(&self) -> u32 {
118 if self.orientation.transposes() {
119 self.height
120 } else {
121 self.width
122 }
123 }
124
125 #[must_use]
127 pub const fn oriented_height(&self) -> u32 {
128 if self.orientation.transposes() {
129 self.width
130 } else {
131 self.height
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
138#[non_exhaustive]
139pub struct ChannelStats {
140 pub min: f64,
142 pub max: f64,
144 pub sum: f64,
146 pub squares_sum: f64,
148 pub mean: f64,
150 pub stdev: f64,
152 pub min_x: u32,
154 pub min_y: u32,
156 pub max_x: u32,
158 pub max_y: u32,
160}
161
162#[derive(Debug, Clone, PartialEq)]
165#[non_exhaustive]
166pub struct Stats {
167 pub channels: Vec<ChannelStats>,
169 pub is_opaque: bool,
171 pub entropy: f64,
173 pub sharpness: f64,
175 pub dominant: [u8; 3],
178}
179
180fn to_linear(v: f64) -> f64 {
182 if v <= 0.040_45 {
183 v / 12.92
184 } else {
185 ((v + 0.055) / 1.055).powf(2.4)
186 }
187}
188
189fn from_linear(v: f64) -> f64 {
191 if v <= 0.003_130_8 {
192 v * 12.92
193 } else {
194 1.055 * v.powf(1.0 / 2.4) - 0.055
195 }
196}
197
198impl Stats {
199 #[must_use]
201 pub fn of(descriptor: &ImageDescriptor, pixels: &[u8]) -> Self {
202 let pixel = descriptor.pixel;
203 let channels = pixel.channels();
204 let (width, height) = (descriptor.width as usize, descriptor.height as usize);
205 let n = width * height;
206 let kind = pixel.sample_kind();
207 let full = match kind {
208 SampleKind::U8 => 255.0,
209 SampleKind::U16 => 65_535.0,
210 _ => 1.0,
211 };
212 let sample = |i: usize| -> f64 {
213 match kind {
214 SampleKind::U8 => f64::from(pixels[i]),
215 SampleKind::U16 => {
216 f64::from(u16::from_ne_bytes([pixels[2 * i], pixels[2 * i + 1]]))
217 }
218 _ => f64::from(f32::from_ne_bytes([
219 pixels[4 * i],
220 pixels[4 * i + 1],
221 pixels[4 * i + 2],
222 pixels[4 * i + 3],
223 ])),
224 }
225 };
226 let mut stats = Vec::with_capacity(channels);
227 for c in 0..channels {
228 let (mut min, mut max, mut sum, mut squares) =
229 (f64::INFINITY, f64::NEG_INFINITY, 0.0, 0.0);
230 let (mut min_at, mut max_at) = (0, 0);
231 for p in 0..n {
232 let v = sample(p * channels + c);
233 if v < min {
234 min = v;
235 min_at = p;
236 }
237 if v > max {
238 max = v;
239 max_at = p;
240 }
241 sum += v;
242 squares += v * v;
243 }
244 let count = n as f64;
245 let mean = if n > 0 { sum / count } else { 0.0 };
246 let variance = if n > 1 {
247 ((squares - sum * sum / count) / (count - 1.0)).max(0.0)
248 } else {
249 0.0
250 };
251 let at = |p: usize| ((p % width.max(1)) as u32, (p / width.max(1)) as u32);
252 let ((min_x, min_y), (max_x, max_y)) = (at(min_at), at(max_at));
253 stats.push(ChannelStats {
254 min: if n > 0 { min } else { 0.0 },
255 max: if n > 0 { max } else { 0.0 },
256 sum,
257 squares_sum: squares,
258 mean,
259 stdev: variance.sqrt(),
260 min_x,
261 min_y,
262 max_x,
263 max_y,
264 });
265 }
266 let layout = pixel.layout();
267 let alpha = layout.has_alpha().then(|| channels - 1);
268 let is_opaque = alpha.is_none_or(|a| stats[a].min >= full);
269 let colour = layout != ChannelLayout::Gray && layout != ChannelLayout::GrayAlpha;
270
271 let levels: usize = if kind == SampleKind::U8 { 256 } else { 65_536 };
274 let top = (levels - 1) as f64;
275 let grey: Vec<u32> = (0..n)
276 .map(|p| {
277 let v = if colour {
278 let ch =
279 |c: usize| to_linear((sample(p * channels + c) / full).clamp(0.0, 1.0));
280 from_linear(0.2126 * ch(0) + 0.7152 * ch(1) + 0.0722 * ch(2))
281 } else {
282 (sample(p * channels) / full).clamp(0.0, 1.0)
283 };
284 (v * top).round() as u32
285 })
286 .collect();
287 let mut histogram = vec![0_u64; levels];
288 for &g in &grey {
289 histogram[g as usize] += 1;
290 }
291 let entropy = histogram
292 .iter()
293 .filter(|&&h| h > 0)
294 .map(|&h| {
295 let p = h as f64 / n as f64;
296 -p * p.log2()
297 })
298 .sum();
299
300 let sharpness = if n > 1 {
303 let at = |x: isize, y: isize| -> f64 {
304 let x = x.clamp(0, width as isize - 1) as usize;
305 let y = y.clamp(0, height as isize - 1) as usize;
306 f64::from(grey[y * width + x])
307 };
308 let (mut s, mut s2) = (0.0, 0.0);
309 for y in 0..height as isize {
310 for x in 0..width as isize {
311 let l =
312 at(x, y - 1) + at(x - 1, y) + at(x + 1, y) + at(x, y + 1) - 4.0 * at(x, y);
313 let v = (l / 9.0).round().clamp(0.0, top);
314 s += v;
315 s2 += v * v;
316 }
317 }
318 let count = n as f64;
319 ((s2 - s * s / count) / (count - 1.0)).max(0.0).sqrt()
320 } else {
321 0.0
322 };
323
324 let mut bins = vec![0_u32; 4096];
326 for p in 0..n {
327 let byte = |c: usize| {
328 ((sample(p * channels + c) / full).clamp(0.0, 1.0) * 255.0).round() as usize
329 };
330 let (r, g, b) = if colour {
331 (byte(0), byte(1), byte(2))
332 } else {
333 (byte(0), byte(0), byte(0))
334 };
335 bins[(r >> 4) * 256 + (g >> 4) * 16 + (b >> 4)] += 1;
336 }
337 let best = bins
338 .iter()
339 .enumerate()
340 .max_by(|a, b| a.1.cmp(b.1).then(b.0.cmp(&a.0)))
341 .map_or(0, |(i, _)| i);
342 let centre = |bin: usize| (bin * 16 + 8) as u8;
343 let dominant = [
344 centre(best / 256),
345 centre(best / 16 % 16),
346 centre(best % 16),
347 ];
348
349 Self {
350 channels: stats,
351 is_opaque,
352 entropy,
353 sharpness,
354 dominant,
355 }
356 }
357}
358
359#[cfg(test)]
360#[allow(
361 clippy::unwrap_used,
362 clippy::float_cmp,
363 reason = "tests operate on known-good values"
364)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn channel_statistics_follow_their_definitions() {
370 let d = ImageDescriptor::new(2, 2, PixelFormat::Rgba8).unwrap();
371 let px = [10, 0, 0, 255, 20, 0, 0, 255, 30, 0, 0, 255, 40, 200, 0, 128];
372 let s = Stats::of(&d, &px);
373 let r = s.channels[0];
374 assert_eq!((r.min, r.max, r.sum, r.mean), (10.0, 40.0, 100.0, 25.0));
375 assert_eq!(r.squares_sum, 100.0 + 400.0 + 900.0 + 1600.0);
376 assert!((r.stdev - (500.0_f64 / 3.0).sqrt()).abs() < 1e-9);
377 assert_eq!((r.min_x, r.min_y, r.max_x, r.max_y), (0, 0, 1, 1));
378 assert!(!s.is_opaque);
379 assert_eq!(s.channels.len(), 4);
380 }
381
382 #[test]
383 fn a_flat_image_has_no_entropy_and_no_sharpness() {
384 let d = ImageDescriptor::new(8, 8, PixelFormat::Rgb8).unwrap();
385 let px = [200_u8, 30, 90].repeat(64);
386 let s = Stats::of(&d, &px);
387 assert_eq!(s.entropy, 0.0);
388 assert_eq!(s.sharpness, 0.0);
389 assert!(s.is_opaque);
390 assert_eq!(s.dominant, [200, 24, 88]);
391 }
392
393 #[test]
394 fn a_two_level_image_has_one_bit_of_entropy_and_edges_are_sharp() {
395 let d = ImageDescriptor::new(8, 8, PixelFormat::Gray8).unwrap();
396 let px: Vec<u8> = (0..64).map(|i| if (i % 8) < 4 { 0 } else { 255 }).collect();
397 let s = Stats::of(&d, &px);
398 assert!((s.entropy - 1.0).abs() < 1e-9);
399 assert!(s.sharpness > 0.0);
400 }
401}