use crate::decode::{Kind, Probe};
use image::ImageFormat;
use indicatif::{ProgressBar, ProgressFinish, ProgressState, ProgressStyle};
use std::time::Duration;
const RULE: &str = " \u{2502} ";
pub struct Progress {
bar: ProgressBar,
}
impl Progress {
pub fn new() -> Progress {
let bar = ProgressBar::new(0).with_finish(ProgressFinish::AndClear);
bar.enable_steady_tick(Duration::from_millis(200));
let p = Progress { bar };
p.spin("scanning");
p
}
pub fn println(&self, line: &str) {
self.bar.suspend(|| eprintln!("{line}"));
}
pub fn spin(&self, what: &str) {
self.bar.set_style(
ProgressStyle::with_template(&format!("{{elapsed_precise}}{RULE}{{spinner}}{RULE}{{msg}}"))
.unwrap()
.tick_chars("\u{280b}\u{2819}\u{2839}\u{2838}\u{283c}\u{2834}\u{2826}\u{2827}\u{2807}\u{280f} "),
);
self.bar.set_message(what.to_string());
}
pub fn count(&self, what: &str, len: u64, unit: &str) -> &ProgressBar {
self.restart(len);
self.bar.set_style(
ProgressStyle::with_template(&format!(
"{{elapsed_precise}}{RULE}[{{bar:28.cyan/blue}}]{RULE}{{percent}}%{RULE}{{human_pos}}/{{human_len}} {unit}{RULE}{{msg}}"
))
.unwrap()
.progress_chars("=>-"),
);
self.bar.set_message(what.to_string());
&self.bar
}
pub fn analysis(&self, work: u64, files: usize) -> &ProgressBar {
self.restart(work);
let mean = work as f64 / files.max(1) as f64;
self.bar.set_style(
ProgressStyle::with_template(&format!(
"{{elapsed_precise}}{RULE}[{{bar:28.cyan/blue}}]{RULE}{{percent}}%{RULE}{{img_rate}}{RULE}{{prefix}}{RULE}{{msg}}"
))
.unwrap()
.with_key("img_rate", move |s: &ProgressState, w: &mut dyn std::fmt::Write| {
let _ = w.write_str(&image_rate(s.per_sec() / mean));
})
.progress_chars("=>-"),
);
self.bar.set_prefix(format!("0/{files}"));
self.bar.set_message(String::new());
&self.bar
}
fn restart(&self, len: u64) {
self.bar.set_length(len);
self.bar.set_position(0);
self.bar.reset_eta();
}
pub fn finish(&self) {
self.bar.finish_and_clear();
}
}
pub fn image_rate(per_sec: f64) -> String {
if !per_sec.is_finite() || per_sec <= 0.0 {
return "-".to_string();
}
let (scaled, unit) = if per_sec >= 1e4 { (per_sec / 1e3, "k img/s") } else { (per_sec, " img/s") };
let number = if scaled >= 100.0 {
format!("{scaled:.0}")
} else if scaled >= 10.0 {
format!("{scaled:.1}")
} else {
format!("{scaled:.2}")
};
if number.len() > 4 {
return "-".to_string();
}
format!("{number}{unit}")
}
pub struct Ticker<'a> {
bar: &'a ProgressBar,
pending: u64,
}
impl<'a> Ticker<'a> {
const BATCH: u64 = 256;
pub fn new(bar: &'a ProgressBar) -> Self {
Ticker { bar, pending: 0 }
}
pub fn tick(&mut self) {
self.pending += 1;
if self.pending >= Self::BATCH {
self.bar.inc(self.pending);
self.pending = 0;
}
}
}
impl Drop for Ticker<'_> {
fn drop(&mut self) {
if self.pending > 0 {
self.bar.inc(self.pending);
}
}
}
pub fn analysis_cost(probe: Option<&Probe>, bytes: u64, work: usize, upsample_below: usize) -> u64 {
let (kind, w, h) = match probe {
Some(p) => (p.kind, p.w as f64, p.h as f64),
None => {
let side = (bytes as f64 / 0.48 / 0.75).sqrt();
(Kind::Image(ImageFormat::Jpeg), side, side * 0.75)
}
};
let (per_px, per_byte) = match kind {
Kind::Image(ImageFormat::Jpeg) => (4.4, 22.0),
Kind::Image(ImageFormat::Png) => (4.5, 8.5),
Kind::Image(ImageFormat::WebP) => (35.0, 0.0),
Kind::Image(ImageFormat::Tiff) => (37.0, 0.0),
Kind::Heif => (40.0, 290.0),
Kind::Jxl => (123.0, 0.0),
_ => (10.0, 0.0),
};
let decode = per_px * w * h + per_byte * bytes as f64;
let long = w.max(h).max(1.0);
let s = if work > 0 && long > work as f64 { work as f64 / long } else { 1.0 };
let (bw, bh) = ((w * s).round().max(1.0), (h * s).round().max(1.0));
let mut factor = 1.0;
while bw.max(bh) * factor * 2.0 <= upsample_below.max(2) as f64 {
factor *= 2.0;
}
let extract = 118.0 * bw * bh * factor * factor;
const PER_FILE: f64 = 200_000.0;
(decode + extract + PER_FILE) as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rate_holds_three_figures_and_climbs_the_ladder() {
assert_eq!(image_rate(412.3), "412 img/s");
assert_eq!(image_rate(56.24), "56.2 img/s");
assert_eq!(image_rate(2.5), "2.50 img/s");
assert_eq!(image_rate(12_345.0), "12.3k img/s");
assert_eq!(image_rate(0.0), "-");
assert_eq!(image_rate(f64::NAN), "-");
}
#[test]
fn cost_follows_the_format_and_the_size() {
let jpeg = |w, h| Probe { kind: Kind::Image(ImageFormat::Jpeg), w, h };
let jxl = Probe { kind: Kind::Jxl, w: 4000, h: 3000 };
let big = analysis_cost(Some(&jpeg(4000, 3000)), 3_000_000, 384, 512);
let small = analysis_cost(Some(&jpeg(224, 224)), 20_000, 384, 512);
let jx = analysis_cost(Some(&jxl), 1_000_000, 384, 512);
assert!(big > 5 * small, "{big} {small}");
assert!(jx > 2 * big, "{jx} {big}");
assert!(small > 118 * 448 * 448, "{small}");
}
}