use crate::config::{Config, LuminanceMethod, Quality};
use crate::grid::{Cell, Rgb};
use crate::ramp::RampType;
#[inline]
pub fn get_pixel_rgb(img_buf: &[u8], img_width: u32, x: u32, y: u32, channels: usize) -> Rgb {
let offset = ((y * img_width + x) as usize) * channels;
if offset + 2 < img_buf.len() {
Rgb::new(img_buf[offset], img_buf[offset + 1], img_buf[offset + 2])
} else {
Rgb::BLACK
}
}
#[inline]
pub fn map_col_geometry(col: u32, grid_width: u32, img_width: u32) -> (u32, u32) {
let x_start = (col as u64 * img_width as u64 / grid_width as u64) as u32;
let x_end = ((((col + 1) as u64 * img_width as u64) / grid_width as u64) as u32)
.max(x_start + 1)
.min(img_width);
(x_start, x_end)
}
#[inline]
pub fn map_row_geometry(row: u32, grid_height: u32, img_height: u32) -> (u32, u32) {
let y_start = (row as u64 * img_height as u64 / grid_height as u64) as u32;
let y_end = ((((row + 1) as u64 * img_height as u64) / grid_height as u64) as u32)
.max(y_start + 1)
.min(img_height);
(y_start, y_end)
}
#[inline]
pub fn map_block_geometry(
col: u32,
row: u32,
grid_width: u32,
grid_height: u32,
img_width: u32,
img_height: u32,
) -> (u32, u32, u32, u32) {
let (x_start, x_end) = map_col_geometry(col, grid_width, img_width);
let (y_start, y_end) = map_row_geometry(row, grid_height, img_height);
(x_start, x_end, y_start, y_end)
}
#[inline]
pub fn average_block(
img_buf: &[u8],
img_width: u32,
channels: usize,
x_start: u32,
x_end: u32,
y_start: u32,
y_end: u32,
) -> Rgb {
if channels == 0 || x_end <= x_start || y_end <= y_start {
return Rgb::BLACK;
}
let mut sum_r: u64 = 0;
let mut sum_g: u64 = 0;
let mut sum_b: u64 = 0;
let row_stride = img_width as usize * channels;
let seg_start = x_start as usize * channels;
let seg_len = (x_end - x_start) as usize * channels;
let mut offset = y_start as usize * row_stride + seg_start;
for _ in y_start..y_end {
if let Some(segment) = img_buf.get(offset..offset.saturating_add(seg_len)) {
for px in segment.chunks_exact(channels) {
sum_r += px[0] as u64;
sum_g += px[1] as u64;
sum_b += px[2] as u64;
}
}
offset += row_stride;
}
let count = ((x_end - x_start) as u64) * ((y_end - y_start) as u64);
Rgb::new(
(sum_r / count) as u8,
(sum_g / count) as u8,
(sum_b / count) as u8,
)
}
#[inline]
fn integer_luminance(method: LuminanceMethod, rgb: Rgb) -> u32 {
let r = rgb.r as u32;
let g = rgb.g as u32;
let b = rgb.b as u32;
match method {
LuminanceMethod::Average => (r + g + b) / 3,
LuminanceMethod::Bt601 => (77 * r + 150 * g + 29 * b) >> 8,
LuminanceMethod::Bt709 => (54 * r + 184 * g + 18 * b) >> 8,
}
}
pub(crate) struct Sampler<'a> {
img_buf: &'a [u8],
img_width: u32,
img_height: u32,
channels: usize,
grid_width: u32,
grid_height: u32,
config: &'a Config,
ramp_chars: &'a [char],
is_light: bool,
max_idx: usize,
max_idx_f: f32,
columns: Vec<(u32, u32)>,
}
impl<'a> Sampler<'a> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
img_buf: &'a [u8],
img_width: u32,
img_height: u32,
channels: usize,
grid_width: u32,
grid_height: u32,
config: &'a Config,
ramp_chars: &'a [char],
) -> Self {
let columns = (0..grid_width)
.map(|col| map_col_geometry(col, grid_width, img_width))
.collect();
let max_idx = ramp_chars.len().saturating_sub(1);
Self {
img_buf,
img_width,
img_height,
channels,
grid_width,
grid_height,
config,
ramp_chars,
is_light: matches!(config.ramp, RampType::Light(_)),
max_idx,
max_idx_f: max_idx as f32,
columns,
}
}
#[inline]
pub(crate) fn row_geometry(&self, row: u32) -> (u32, u32) {
map_row_geometry(row, self.grid_height, self.img_height)
}
#[inline]
pub(crate) fn fill_row(&self, row_idx: u32, row_cells: &mut [Cell]) {
let (y_start, y_end) = self.row_geometry(row_idx);
let mut diffusion_error = 0.0f32;
for (col, cell) in row_cells.iter_mut().enumerate() {
*cell = self.cell(col as u32, y_start, y_end, &mut diffusion_error);
}
}
#[inline]
pub(crate) fn cell(
&self,
col: u32,
y_start: u32,
y_end: u32,
diffusion_error: &mut f32,
) -> Cell {
let (x_start, x_end) = match self.columns.get(col as usize) {
Some(&geom) => geom,
None => map_col_geometry(col, self.grid_width, self.img_width),
};
let avg_rgb = average_block(
self.img_buf,
self.img_width,
self.channels,
x_start,
x_end,
y_start,
y_end,
);
let idx = match self.config.quality {
Quality::Good => {
let mut lum = integer_luminance(self.config.luminance_method, avg_rgb);
if self.is_light {
lum = 255 - lum;
}
if self.max_idx == 0 {
0
} else {
((lum as usize * self.max_idx) + 127) / 255
}
}
Quality::Better => {
let mut lum = self
.config
.luminance_method
.calculate(avg_rgb.r, avg_rgb.g, avg_rgb.b);
if self.is_light {
lum = 1.0 - lum;
}
(lum.clamp(0.0, 1.0) * self.max_idx_f).round() as usize
}
Quality::Best => {
let mut lum = self
.config
.luminance_method
.calculate(avg_rgb.r, avg_rgb.g, avg_rgb.b);
if self.is_light {
lum = 1.0 - lum;
}
let adjusted = (lum + *diffusion_error).clamp(0.0, 1.0);
let exact = adjusted * self.max_idx_f;
let idx = exact.round().clamp(0.0, self.max_idx_f) as usize;
*diffusion_error = if self.max_idx_f > 0.0 {
((exact - idx as f32) / self.max_idx_f).clamp(-1.0, 1.0)
} else {
0.0
};
idx
}
};
let ch = self.ramp_chars.get(idx).copied().unwrap_or(' ');
Cell::new(ch, avg_rgb)
}
}
#[allow(clippy::too_many_arguments)]
pub fn fill_row(
row_idx: u32,
row_cells: &mut [Cell],
img_buf: &[u8],
img_width: u32,
img_height: u32,
channels: usize,
grid_width: u32,
grid_height: u32,
config: &Config,
ramp_chars: &[char],
) {
let sampler = Sampler::new(
img_buf,
img_width,
img_height,
channels,
grid_width,
grid_height,
config,
ramp_chars,
);
sampler.fill_row(row_idx, row_cells);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pixel_and_block_sampling() {
let img_buf = vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255];
let oob = get_pixel_rgb(&img_buf, 2, 5, 5, 3);
assert_eq!(oob, Rgb::BLACK);
let (x_start, x_end, y_start, y_end) = map_block_geometry(0, 0, 1, 1, 2, 2);
assert_eq!((x_start, x_end, y_start, y_end), (0, 2, 0, 2));
let avg = average_block(&img_buf, 2, 3, x_start, x_end, y_start, y_end);
assert_eq!(avg, Rgb::new(127, 127, 127));
}
#[test]
fn column_and_row_geometry_match_block_geometry() {
for col in 0..7u32 {
for row in 0..5u32 {
let (xs, xe, ys, ye) = map_block_geometry(col, row, 7, 5, 96, 47);
assert_eq!(map_col_geometry(col, 7, 96), (xs, xe));
assert_eq!(map_row_geometry(row, 5, 47), (ys, ye));
}
}
}
#[test]
fn average_block_ignores_out_of_range_rows() {
let buf = vec![255, 255, 255];
let avg = average_block(&buf, 1, 3, 0, 1, 0, 4);
assert_eq!(avg, Rgb::new(63, 63, 63)); }
}