1use crate::binary::{be_f64_at, be_u32_at};
44use crate::image::RgbaImage;
45
46#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
48pub struct Tone {
49 pub lines: f64,
51 pub resolution: f64,
53 pub angle: f64,
55 pub shape: u32,
57 pub mode: u32,
59 pub density: f64,
61 pub unknown: (u32, u32),
63}
64
65const ROUND: u32 = 1;
67const SQUARE: u32 = 2;
68const LINES: u32 = 4;
69const FROM_IMAGE: u32 = 0;
71const FIXED: u32 = 2;
72
73impl Tone {
74 pub fn parse(section: &[u8]) -> Option<Tone> {
76 if section.len() < 60 || be_u32_at(section, 0) == 0 {
77 return None;
78 }
79 let tone = Tone {
80 resolution: be_f64_at(section, 4),
81 shape: be_u32_at(section, 12),
82 mode: be_u32_at(section, 16),
83 density: be_u32_at(section, 20) as f64 / 100.0,
84 lines: be_f64_at(section, 24),
85 angle: be_u32_at(section, 40) as f64,
86 unknown: (be_u32_at(section, 52), be_u32_at(section, 56)),
87 };
88 (tone.lines > 0.0 && tone.resolution > 0.0 && tone.resolution / tone.lines >= 1.0).then_some(tone)
89 }
90
91 pub fn is_approximate(&self) -> bool {
95 !matches!(self.shape, ROUND | SQUARE) || !matches!(self.mode, FROM_IMAGE | FIXED) || self.unknown != (0, 0)
96 }
97
98 fn lattice(&self) -> [(f64, f64); 2] {
100 let period = self.resolution / self.lines;
101 let (sin, cos) = self.angle.to_radians().sin_cos();
102 let a = ((period * cos).round(), (period * sin).round());
103 let a = if a.0 == 0.0 && a.1 == 0.0 { (1.0, 0.0) } else { a };
105 [a, (-a.1, a.0)]
106 }
107
108 pub fn draw(&self, image: &RgbaImage, origin: (i64, i64)) -> RgbaImage {
110 let [a, b] = self.lattice();
111 let det = a.0 * b.1 - a.1 * b.0;
112 let inverse = [b.1 / det, -b.0 / det, -a.1 / det, a.0 / det];
114 let fixed = self.mode == FIXED;
115 let shape = match self.shape {
116 _ if fixed => Shape::Circle,
117 SQUARE => Shape::Square,
118 LINES => Shape::Lines,
119 _ => Shape::Round,
120 };
121 let table = Thresholds::new(shape);
122 let mut out = RgbaImage::new(image.width, image.height);
123 for y in 0..image.height {
124 let cy = (origin.1 + y as i64) as f64;
125 for x in 0..image.width {
126 let p = image.pixel(x, y);
127 if p[3] == 0 {
128 continue;
129 }
130 let cx = (origin.0 + x as i64) as f64;
131 let s = inverse[0] * cx + inverse[1] * cy;
132 let r = inverse[2] * cx + inverse[3] * cy;
133 let t = table.at(s - s.floor(), r - r.floor());
134 let pixel = if fixed {
135 (self.density > t).then_some([0, 0, 0, 255])
136 } else if p[3] as f64 / 255.0 > t {
137 let alpha = p[3] as f64 / 255.0;
138 let light = (0.114 * p[0] as f64 + 0.299 * p[1] as f64 + 0.587 * p[2] as f64) / 255.0;
139 Some(if (1.0 - light) * alpha > t { [0, 0, 0, 255] } else { [255, 255, 255, 255] })
140 } else {
141 None
142 };
143 if let Some(pixel) = pixel {
144 out.set_pixel(x, y, pixel);
145 }
146 }
147 }
148 out
149 }
150}
151
152#[derive(Clone, Copy)]
153enum Shape {
154 Round,
155 Square,
156 Lines,
157 Circle,
158}
159
160struct Thresholds {
163 values: Vec<f32>,
164}
165
166const GRID: usize = 256;
167
168impl Thresholds {
169 fn new(shape: Shape) -> Self {
170 let spot = |s: f64, r: f64| -> f64 {
171 let (u, v) = ((2.0 * s - 1.0).abs(), (2.0 * r - 1.0).abs());
173 match shape {
174 Shape::Round if u + v <= 1.0 => 1.0 - (u * u + v * v) / 2.0,
175 Shape::Round => ((u - 1.0).powi(2) + (v - 1.0).powi(2)) / 2.0,
176 Shape::Square => -u.max(v),
177 Shape::Lines => -v,
178 Shape::Circle => -(u * u + v * v),
179 }
180 };
181 let centre = |i: usize| (i as f64 + 0.5) / GRID as f64;
182 let spots: Vec<f64> = (0..GRID * GRID).map(|i| spot(centre(i % GRID), centre(i / GRID))).collect();
183 let mut order: Vec<usize> = (0..spots.len()).collect();
184 order.sort_by(|&i, &j| spots[j].total_cmp(&spots[i]));
185 let mut values = vec![0f32; spots.len()];
186 let count = spots.len() as f32;
187 let mut start = 0;
189 while start < order.len() {
190 let mut end = start + 1;
191 while end < order.len() && spots[order[end]] == spots[order[start]] {
192 end += 1;
193 }
194 for &i in &order[start..end] {
195 values[i] = start as f32 / count;
196 }
197 start = end;
198 }
199 Thresholds { values }
200 }
201
202 fn at(&self, s: f64, r: f64) -> f64 {
204 let i = ((s * GRID as f64) as usize).min(GRID - 1);
205 let j = ((r * GRID as f64) as usize).min(GRID - 1);
206 self.values[j * GRID + i] as f64
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn section(lines: f64, angle: u32, shape: u32, mode: u32, density: u32) -> Vec<u8> {
215 let mut out = Vec::new();
216 out.extend_from_slice(&1u32.to_be_bytes());
217 out.extend_from_slice(&600f64.to_be_bytes());
218 for word in [shape, mode, density] {
219 out.extend_from_slice(&word.to_be_bytes());
220 }
221 out.extend_from_slice(&lines.to_be_bytes());
222 for word in [1, 0, angle, 30, 0, 0, 0, 0, 0, 0, 0] {
223 out.extend_from_slice(&u32::to_be_bytes(word));
224 }
225 out
226 }
227
228 fn coverage(image: &RgbaImage) -> f64 {
229 let black = image.pixels.as_chunks::<4>().0.iter().filter(|p| p[0] == 0 && p[3] == 255).count();
230 black as f64 / (image.width * image.height) as f64
231 }
232
233 #[test]
234 fn grey_becomes_dots_of_its_darkness() {
235 let tone = Tone::parse(§ion(20.0, 45, 1, 0, 20)).unwrap();
236 assert_eq!(tone.lattice(), [(21.0, 21.0), (-21.0, 21.0)]);
237 let mut image = RgbaImage::new(84, 84);
238 image.fill([191, 191, 191, 255]);
239 let drawn = tone.draw(&image, (0, 0));
240 assert!((coverage(&drawn) - 0.25).abs() < 0.02, "{}", coverage(&drawn));
241 assert_eq!(drawn.pixel(0, 21), [0, 0, 0, 255]);
243 assert_eq!(drawn.pixel(0, 0), [255, 255, 255, 255]);
244 }
245
246 #[test]
247 fn darkness_is_weighted_by_alpha_and_the_rest_is_white_or_transparent() {
248 let tone = Tone::parse(§ion(60.0, 45, 1, 0, 20)).unwrap();
249 let mut image = RgbaImage::new(70, 70);
250 image.fill([128, 128, 128, 153]);
252 let drawn = tone.draw(&image, (0, 0));
253 let white = drawn.pixels.as_chunks::<4>().0.iter().filter(|p| **p == [255, 255, 255, 255]).count();
254 assert!((coverage(&drawn) - 0.3).abs() < 0.03, "{}", coverage(&drawn));
255 assert!((white as f64 / 4900.0 - 0.3).abs() < 0.03);
256 image.fill([255, 0, 0, 255]);
258 assert!((coverage(&tone.draw(&image, (0, 0))) - 0.886).abs() < 0.03);
259 }
260
261 #[test]
262 fn fixed_density_ignores_the_colour_and_leaves_the_rest_transparent() {
263 let tone = Tone::parse(§ion(20.0, 0, 1, 2, 30)).unwrap();
264 let mut image = RgbaImage::new(60, 60);
265 image.fill([255, 255, 255, 40]);
266 let drawn = tone.draw(&image, (0, 0));
267 assert!((coverage(&drawn) - 0.3).abs() < 0.02);
268 assert_eq!(drawn.pixel(0, 0)[3], 0);
269 assert_eq!(drawn.pixel(15, 15), [0, 0, 0, 255]);
270 }
271
272 #[test]
273 fn switched_off_tone_is_none() {
274 let mut data = section(20.0, 45, 1, 0, 20);
275 data[3] = 0;
276 assert!(Tone::parse(&data).is_none());
277 }
278}