use crate::binary::{be_f64_at, be_u32_at};
use crate::image::RgbaImage;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
pub struct Tone {
pub lines: f64,
pub resolution: f64,
pub angle: f64,
pub shape: u32,
pub mode: u32,
pub density: f64,
pub unknown: (u32, u32),
}
const ROUND: u32 = 1;
const SQUARE: u32 = 2;
const LINES: u32 = 4;
const FROM_IMAGE: u32 = 0;
const FIXED: u32 = 2;
impl Tone {
pub fn parse(section: &[u8]) -> Option<Tone> {
if section.len() < 60 || be_u32_at(section, 0) == 0 {
return None;
}
let tone = Tone {
resolution: be_f64_at(section, 4),
shape: be_u32_at(section, 12),
mode: be_u32_at(section, 16),
density: be_u32_at(section, 20) as f64 / 100.0,
lines: be_f64_at(section, 24),
angle: be_u32_at(section, 40) as f64,
unknown: (be_u32_at(section, 52), be_u32_at(section, 56)),
};
(tone.lines > 0.0 && tone.resolution > 0.0 && tone.resolution / tone.lines >= 1.0).then_some(tone)
}
pub fn is_approximate(&self) -> bool {
!matches!(self.shape, ROUND | SQUARE) || !matches!(self.mode, FROM_IMAGE | FIXED) || self.unknown != (0, 0)
}
fn lattice(&self) -> [(f64, f64); 2] {
let period = self.resolution / self.lines;
let (sin, cos) = self.angle.to_radians().sin_cos();
let a = ((period * cos).round(), (period * sin).round());
let a = if a.0 == 0.0 && a.1 == 0.0 { (1.0, 0.0) } else { a };
[a, (-a.1, a.0)]
}
pub fn draw(&self, image: &RgbaImage, origin: (i64, i64)) -> RgbaImage {
let [a, b] = self.lattice();
let det = a.0 * b.1 - a.1 * b.0;
let inverse = [b.1 / det, -b.0 / det, -a.1 / det, a.0 / det];
let fixed = self.mode == FIXED;
let shape = match self.shape {
_ if fixed => Shape::Circle,
SQUARE => Shape::Square,
LINES => Shape::Lines,
_ => Shape::Round,
};
let table = Thresholds::new(shape);
let mut out = RgbaImage::new(image.width, image.height);
for y in 0..image.height {
let cy = (origin.1 + y as i64) as f64;
for x in 0..image.width {
let p = image.pixel(x, y);
if p[3] == 0 {
continue;
}
let cx = (origin.0 + x as i64) as f64;
let s = inverse[0] * cx + inverse[1] * cy;
let r = inverse[2] * cx + inverse[3] * cy;
let t = table.at(s - s.floor(), r - r.floor());
let pixel = if fixed {
(self.density > t).then_some([0, 0, 0, 255])
} else if p[3] as f64 / 255.0 > t {
let alpha = p[3] as f64 / 255.0;
let light = (0.114 * p[0] as f64 + 0.299 * p[1] as f64 + 0.587 * p[2] as f64) / 255.0;
Some(if (1.0 - light) * alpha > t { [0, 0, 0, 255] } else { [255, 255, 255, 255] })
} else {
None
};
if let Some(pixel) = pixel {
out.set_pixel(x, y, pixel);
}
}
}
out
}
}
#[derive(Clone, Copy)]
enum Shape {
Round,
Square,
Lines,
Circle,
}
struct Thresholds {
values: Vec<f32>,
}
const GRID: usize = 256;
impl Thresholds {
fn new(shape: Shape) -> Self {
let spot = |s: f64, r: f64| -> f64 {
let (u, v) = ((2.0 * s - 1.0).abs(), (2.0 * r - 1.0).abs());
match shape {
Shape::Round if u + v <= 1.0 => 1.0 - (u * u + v * v) / 2.0,
Shape::Round => ((u - 1.0).powi(2) + (v - 1.0).powi(2)) / 2.0,
Shape::Square => -u.max(v),
Shape::Lines => -v,
Shape::Circle => -(u * u + v * v),
}
};
let centre = |i: usize| (i as f64 + 0.5) / GRID as f64;
let spots: Vec<f64> = (0..GRID * GRID).map(|i| spot(centre(i % GRID), centre(i / GRID))).collect();
let mut order: Vec<usize> = (0..spots.len()).collect();
order.sort_by(|&i, &j| spots[j].total_cmp(&spots[i]));
let mut values = vec![0f32; spots.len()];
let count = spots.len() as f32;
let mut start = 0;
while start < order.len() {
let mut end = start + 1;
while end < order.len() && spots[order[end]] == spots[order[start]] {
end += 1;
}
for &i in &order[start..end] {
values[i] = start as f32 / count;
}
start = end;
}
Thresholds { values }
}
fn at(&self, s: f64, r: f64) -> f64 {
let i = ((s * GRID as f64) as usize).min(GRID - 1);
let j = ((r * GRID as f64) as usize).min(GRID - 1);
self.values[j * GRID + i] as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
fn section(lines: f64, angle: u32, shape: u32, mode: u32, density: u32) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&1u32.to_be_bytes());
out.extend_from_slice(&600f64.to_be_bytes());
for word in [shape, mode, density] {
out.extend_from_slice(&word.to_be_bytes());
}
out.extend_from_slice(&lines.to_be_bytes());
for word in [1, 0, angle, 30, 0, 0, 0, 0, 0, 0, 0] {
out.extend_from_slice(&u32::to_be_bytes(word));
}
out
}
fn coverage(image: &RgbaImage) -> f64 {
let black = image.pixels.as_chunks::<4>().0.iter().filter(|p| p[0] == 0 && p[3] == 255).count();
black as f64 / (image.width * image.height) as f64
}
#[test]
fn grey_becomes_dots_of_its_darkness() {
let tone = Tone::parse(§ion(20.0, 45, 1, 0, 20)).unwrap();
assert_eq!(tone.lattice(), [(21.0, 21.0), (-21.0, 21.0)]);
let mut image = RgbaImage::new(84, 84);
image.fill([191, 191, 191, 255]);
let drawn = tone.draw(&image, (0, 0));
assert!((coverage(&drawn) - 0.25).abs() < 0.02, "{}", coverage(&drawn));
assert_eq!(drawn.pixel(0, 21), [0, 0, 0, 255]);
assert_eq!(drawn.pixel(0, 0), [255, 255, 255, 255]);
}
#[test]
fn darkness_is_weighted_by_alpha_and_the_rest_is_white_or_transparent() {
let tone = Tone::parse(§ion(60.0, 45, 1, 0, 20)).unwrap();
let mut image = RgbaImage::new(70, 70);
image.fill([128, 128, 128, 153]);
let drawn = tone.draw(&image, (0, 0));
let white = drawn.pixels.as_chunks::<4>().0.iter().filter(|p| **p == [255, 255, 255, 255]).count();
assert!((coverage(&drawn) - 0.3).abs() < 0.03, "{}", coverage(&drawn));
assert!((white as f64 / 4900.0 - 0.3).abs() < 0.03);
image.fill([255, 0, 0, 255]);
assert!((coverage(&tone.draw(&image, (0, 0))) - 0.886).abs() < 0.03);
}
#[test]
fn fixed_density_ignores_the_colour_and_leaves_the_rest_transparent() {
let tone = Tone::parse(§ion(20.0, 0, 1, 2, 30)).unwrap();
let mut image = RgbaImage::new(60, 60);
image.fill([255, 255, 255, 40]);
let drawn = tone.draw(&image, (0, 0));
assert!((coverage(&drawn) - 0.3).abs() < 0.02);
assert_eq!(drawn.pixel(0, 0)[3], 0);
assert_eq!(drawn.pixel(15, 15), [0, 0, 0, 255]);
}
#[test]
fn switched_off_tone_is_none() {
let mut data = section(20.0, 45, 1, 0, 20);
data[3] = 0;
assert!(Tone::parse(&data).is_none());
}
}