1use crate::prelude::*;
23
24use serde::Deserialize;
25
26use super::encode;
27use crate::image::Image;
28use crate::import_settings::ImportSettings;
29use crate::onebit::BAYER;
30#[cfg(feature = "png")]
31use {
32 super::OneBitPicture,
33 crate::error::ContentError,
34 crate::format::{AssetPath, DecodeCx, Format},
35 crate::image::{MAX_PNG_BYTES, PNG_SIGNATURE, Png},
36};
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Deserialize)]
40#[serde(rename_all = "snake_case")]
41#[non_exhaustive]
42pub enum Dither {
43 #[default]
50 Atkinson,
51 Bayer,
57 Threshold(u8),
59 None,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
68#[serde(default, deny_unknown_fields)]
69#[non_exhaustive]
70pub struct OneBitSettings {
71 pub dither: Dither,
73 pub alpha_cut: u8,
75 pub invert: bool,
78}
79
80impl OneBitSettings {
81 #[must_use]
83 pub const fn new(dither: Dither) -> Self {
84 Self {
85 dither,
86 alpha_cut: 128,
87 invert: false,
88 }
89 }
90}
91
92impl Default for OneBitSettings {
93 fn default() -> Self {
94 Self::new(Dither::Atkinson)
95 }
96}
97
98impl ImportSettings for OneBitSettings {
99 const SECTION: &'static str = "onebit";
100}
101
102pub fn brightness([r, g, b, _]: [u8; 4]) -> u8 {
104 let sum = 77 * u32::from(r) + 150 * u32::from(g) + 29 * u32::from(b) + 128;
105 u8::try_from(sum >> 8).unwrap_or(u8::MAX)
107}
108
109pub fn dither(image: &Image, settings: OneBitSettings) -> Result<Vec<u8>, String> {
115 let too_big = || {
116 format!(
117 "{}×{} px is too big for a one-bit picture",
118 image.width, image.height
119 )
120 };
121 let width = u16::try_from(image.width).map_err(|_| too_big())?;
122 let height = u16::try_from(image.height).map_err(|_| too_big())?;
123 let size = usize::from(width) * usize::from(height);
124 if width == 0 || height == 0 {
125 return Err(format!(
126 "a {}×{} px picture has no pixels",
127 image.width, image.height
128 ));
129 }
130 if image.rgba.len() != size * 4 {
131 return Err(format!(
132 "a {}×{} px picture needs {} bytes of pixels, and has {}",
133 image.width,
134 image.height,
135 size * 4,
136 image.rgba.len()
137 ));
138 }
139 let pixels = image.rgba.as_chunks::<4>().0;
140 let opaque: Vec<bool> = pixels.iter().map(|p| p[3] >= settings.alpha_cut).collect();
141 let light: Vec<u8> = pixels
142 .iter()
143 .map(|&p| {
144 let b = brightness(p);
145 if settings.invert { 255 - b } else { b }
146 })
147 .collect();
148 let picture = Picture {
149 width: usize::from(width),
150 opaque: &opaque,
151 };
152 let white = match settings.dither {
153 Dither::Atkinson => picture.atkinson(&light),
154 Dither::Bayer => picture.map(&light, |l, x, y| {
155 u16::from(BAYER[y % 8][x % 8]) * 4 + 2 >= u16::from(255 - l)
156 }),
157 Dither::Threshold(cut) => picture.map(&light, |l, _, _| l >= cut),
158 Dither::None => {
159 if let Some(at) = (0..size).find(|&i| opaque[i] && !is_black_or_white(pixels[i])) {
160 let [r, g, b, _] = pixels[at];
161 return Err(format!(
162 "the picture isn't black and white, as `dither: none` needs: the pixel at \
163 column {}, row {} is #{r:02x}{g:02x}{b:02x}",
164 at % picture.width,
165 at / picture.width
166 ));
167 }
168 picture.map(&light, |l, _, _| l >= 128)
169 }
170 };
171 let style = rows(picture.width, &white);
172 let mask = opaque
173 .contains(&false)
174 .then(|| rows(picture.width, &opaque));
175 encode((width, height), (width, height), &[&style], mask.as_deref())
176 .map_err(ToString::to_string)
177}
178
179#[cfg(feature = "png")]
185#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
186pub struct Dithered;
187
188#[cfg(feature = "png")]
189impl Format for Dithered {
190 type Asset = OneBitPicture;
191
192 fn id(&self) -> &'static str {
193 "dithered"
194 }
195
196 fn extensions(&self) -> &'static [&'static str] {
197 &["png"]
198 }
199
200 fn sniff(&self, bytes: &[u8]) -> bool {
201 bytes.starts_with(PNG_SIGNATURE)
202 }
203
204 fn max_bytes(&self) -> usize {
205 MAX_PNG_BYTES
206 }
207
208 fn decode(
209 &self,
210 file: &AssetPath,
211 bytes: &[u8],
212 cx: &DecodeCx<'_>,
213 ) -> Result<OneBitPicture, Vec<ContentError>> {
214 let image = Png.decode(file, bytes, cx)?;
215 let settings = cx.import_settings::<OneBitSettings>(file)?;
216 let error = |message: String| vec![ContentError::new(file.shown(), message)];
217 let made = dither(&image, settings).map_err(error)?;
218 OneBitPicture::new(made).map_err(|e| error(e.to_owned()))
219 }
220}
221
222fn is_black_or_white([r, g, b, _]: [u8; 4]) -> bool {
224 r == g && g == b && (r == 0 || r == 255)
225}
226
227struct Picture<'a> {
230 width: usize,
231 opaque: &'a [bool],
232}
233
234impl Picture<'_> {
235 fn map(&self, light: &[u8], white: impl Fn(u8, usize, usize) -> bool) -> Vec<bool> {
238 light
239 .iter()
240 .enumerate()
241 .map(|(i, &l)| !self.opaque[i] || white(l, i % self.width, i / self.width))
242 .collect()
243 }
244
245 fn atkinson(&self, light: &[u8]) -> Vec<bool> {
249 const SPREAD: [(isize, usize); 6] = [(1, 0), (2, 0), (-1, 1), (0, 1), (1, 1), (0, 2)];
251 let height = light.len() / self.width;
252 let mut level: Vec<i16> = light.iter().map(|&l| i16::from(l)).collect();
255 let mut white = vec![true; light.len()];
256 for y in 0..height {
257 for x in 0..self.width {
258 let i = y * self.width + x;
259 if !self.opaque[i] {
260 continue;
261 }
262 let old = level[i];
263 white[i] = old >= 128;
264 let share = (old - if white[i] { 255 } else { 0 }) / 8;
265 for (dx, dy) in SPREAD {
266 let Some(nx) = x.checked_add_signed(dx).filter(|&nx| nx < self.width) else {
267 continue;
268 };
269 let n = (y + dy) * self.width + nx;
270 if y + dy < height && self.opaque[n] {
271 level[n] += share;
272 }
273 }
274 }
275 }
276 white
277 }
278}
279
280fn rows(width: usize, set: &[bool]) -> Vec<u8> {
283 let stride = width.div_ceil(8);
284 let mut rows = vec![0u8; set.len() / width * stride];
285 for (i, _) in set.iter().enumerate().filter(|&(_, &on)| on) {
286 let (x, y) = (i % width, i / width);
287 rows[y * stride + x / 8] |= 0x80 >> (x % 8);
288 }
289 rows
290}
291
292#[cfg(test)]
293pub(crate) mod tests;