ch_ar_t/
lib.rs

1use anyhow::Error;
2use itertools::Itertools;
3use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
4mod conversions;
5pub use conversions::*;
6
7const ASCII_SIZE: usize = 8;
8const DEFAULT_TEXTURE: &str = " .,:-=*#@";
9pub const UNICODE_TEXTURE: &str = " ░▒▓█";
10pub const MIXED_TEXTURE: &str = " .-=*#@░▒▓█";
11
12pub struct ImageRepr {
13    pub image: zune_image::image::Image,
14    pub dimensions: ImgDimensions,
15}
16
17impl ImageRepr {
18    pub fn width(&self) -> usize {
19        self.dimensions.width
20    }
21    pub fn height(&self) -> usize {
22        self.dimensions.height
23    }
24}
25
26pub struct ImgDimensions {
27    pub width: usize,
28    pub height: usize,
29}
30
31pub struct AppState {
32    pub og_image: ImageRepr,
33    pub resized_image: Option<ImageRepr>,
34    pub ascii_size: usize,
35    pub texture: String,
36}
37
38impl AppState {
39    pub fn new(buffer: &[u8], resize_width: Option<usize>) -> anyhow::Result<Self> {
40        let decode_options = DecoderOptions::default();
41
42        let image = zune_image::image::Image::read(buffer, decode_options).unwrap();
43
44        let meta = image.metadata();
45        let (width, height) = meta.get_dimensions();
46
47        let mut state = AppState {
48            og_image: ImageRepr {
49                image,
50                dimensions: ImgDimensions { width, height },
51            },
52            ascii_size: ASCII_SIZE,
53            resized_image: None,
54            texture: DEFAULT_TEXTURE.to_owned(),
55        };
56
57        let aspect_ratio = width / height;
58
59        let resize_dimensions = resize_width.and_then(|w| {
60            Some(ImgDimensions {
61                width: w,
62                height: w / aspect_ratio,
63            })
64        });
65        state.resize(resize_dimensions)?;
66
67        Ok(state)
68    }
69
70    pub fn set_pixel_size(&mut self, ascii_pixel_size: usize) {
71        self.ascii_size = ascii_pixel_size;
72    }
73
74    pub fn set_texture(&mut self, texture: &str) {
75        self.texture = texture.to_owned()
76    }
77
78    fn resize(&mut self, dimensions: Option<ImgDimensions>) -> anyhow::Result<()> {
79        let og_dims = &self.og_image.dimensions;
80        let out_dims = dimensions.or_else(|| {
81            term_size::dimensions().map(|d| ImgDimensions {
82                width: d.0,
83                height: d.1,
84            })
85        });
86        let out_dims = out_dims.ok_or(Error::msg("failed to get term dimentions"))?;
87        let (resized_width, resized_height) = if og_dims.width > og_dims.height {
88            let new_width = out_dims.width - ASCII_SIZE;
89            let new_height = (new_width * og_dims.height) / og_dims.width;
90            (new_width, new_height)
91        } else {
92            let new_height = out_dims.height - ASCII_SIZE;
93            let new_width = (new_height * og_dims.width) / og_dims.height;
94            (new_width, new_height)
95        };
96        let resized = resize_image(&self.og_image.image, resized_width, resized_height);
97
98        self.resized_image = Some(ImageRepr {
99            image: resized,
100            dimensions: ImgDimensions {
101                width: resized_width,
102                height: resized_height,
103            },
104        });
105        Ok(())
106    }
107
108    fn resized_rgb_channels(&self) -> anyhow::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
109        if let Some(resized) = self.resized_image.as_ref() {
110            let channels = resized
111                .image
112                .channels_ref(true)
113                .into_iter()
114                .map(|c| c.reinterpret_as::<u8>())
115                .collect::<Result<Vec<_>, _>>()
116                .map_err(|_| anyhow::Error::msg("cannot get channels"))?;
117            //rgb
118            Ok((
119                channels[0].to_vec(),
120                channels[1].to_vec(),
121                channels[2].to_vec(),
122            ))
123        } else {
124            Err(anyhow::Error::msg("please resize image first"))
125        }
126    }
127
128    fn quantized_level(&self, value: u8) -> u32 {
129        // distance between each bins (in our case 255 / 8  will give us 8 bins or 8 quantized levels)
130        // 255 - max luma value, 0 - min
131        let distance_bw_levels = (255 - 0) / self.texture.chars().count() as u32;
132        // divide the value with quatized level to bring it to one of the 8 value range
133        let value = value as u32 / distance_bw_levels;
134        // for indexing it to 0
135        value.saturating_sub(1)
136    }
137
138    fn to_luma(&self) -> anyhow::Result<Vec<u8>> {
139        let (r, g, b) = self.resized_rgb_channels()?;
140        Ok(itertools::izip!(r, g, b)
141            // convert rgb to luma
142            .map(|(r, g, b)| (0.2126 * r as f32 + 0.7152 * g as f32 + 0.0722 * b as f32) as u8)
143            .collect::<Vec<_>>())
144    }
145
146    pub fn apply_texture(&self) -> anyhow::Result<String> {
147        let luma = self.to_luma()?;
148        let mapped_texture = self.texture.chars().collect::<Vec<_>>();
149        let l = luma
150            .iter()
151            .map(|&l| {
152                mapped_texture
153                    .get(self.quantized_level(l) as usize)
154                    .unwrap()
155            })
156            .collect::<Vec<_>>();
157        Ok(l.chunks(self.resized_image.as_ref().unwrap().width())
158            .map(|cs| cs.iter().join(""))
159            .join("\n"))
160    }
161}