image_reducer/utils/
mod.rs

1use std::{
2    hash::{BuildHasher, BuildHasherDefault, Hasher},
3    io::Write,
4};
5
6use bytesize::ByteSize;
7use chrono::Local;
8use env_logger::fmt::Formatter;
9use log::{Level, LevelFilter, Record};
10use oxipng::{optimize_from_memory, Options};
11use twox_hash::XxHash64;
12
13use colored::Colorize;
14
15use crate::{errors::TinyError, TinyResult};
16
17pub struct ImageBuffer {
18    pub output: Vec<u8>,
19    pub before: ByteSize,
20    pub after: ByteSize,
21    pub reduce: f64,
22}
23
24pub fn optimize_png(png: &[u8]) -> TinyResult<ImageBuffer> {
25    let opts = Options { ..Options::default() };
26    let image = optimize_from_memory(png, &opts)?;
27    let before = ByteSize::b(png.len() as u64);
28    let after = ByteSize::b(image.len() as u64);
29    let reduce = calc_reduce(png, &image);
30    if is_fully_optimized(png.len(), image.len(), &opts) {
31        return Err(TinyError::ImageOptimized);
32    }
33    Ok(ImageBuffer { output: image, before, after, reduce })
34}
35
36pub fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
37    original_size <= optimized_size && opts.interlace.is_none()
38}
39
40pub fn calc_reduce(before: &[u8], after: &[u8]) -> f64 {
41    let before = before.len() as f64;
42    let after = after.len() as f64;
43    100.0 * (after - before) / before
44}
45
46pub fn hash_file(image: &[u8]) -> u64 {
47    let mut hasher: XxHash64 = BuildHasherDefault::default().build_hasher();
48    hasher.write(image);
49    hasher.finish()
50}
51
52pub fn logger(level: LevelFilter) {
53    let _ = env_logger::builder()
54        .format_module_path(false)
55        .format(log_writter)
56        .filter(Some("oxipng"), LevelFilter::Off)
57        .filter_level(level)
58        // .is_test(cfg!(debug_assertions))
59        .try_init();
60}
61
62pub fn log_writter(w: &mut Formatter, record: &Record) -> std::io::Result<()> {
63    let header = match record.level() {
64        Level::Error => "Error".red(),
65        Level::Warn => "Warn ".yellow(),
66        Level::Info => "Info ".green(),
67        Level::Debug => "Debug".green(),
68        Level::Trace => "Trace".green(),
69    };
70    let logs = format!("[{header}{}] {}", Local::now().format("%Y-%d-%m %H:%M:%S"), record.args());
71    for (i, line) in logs.lines().enumerate() {
72        if i != 0 {
73            w.write(b"\n")?;
74            w.write(b"    ")?;
75        }
76        w.write(line.as_bytes())?;
77    }
78    w.write(b"\n")?;
79    Ok(())
80}