Skip to main content

shuyi_assets/bitmap/
dither.rs

1//! Making a colour picture one-bit at bake time, in integers (ADR-0008
2//! §4): `shuyi bake --target playdate` makes every picture a one-bit
3//! picture file once, so the device never dithers. The same picture and
4//! settings give the same file on every platform, so the baked file is
5//! tested equal to dithering its source again (ADR-0007 §4).
6//!
7//! How a picture is made one-bit is its import settings' `onebit`
8//! section ([`OneBitSettings`], ADR-0057): `images/hero.png.meta` holding
9//! `(onebit: (dither: bayer))` sets one picture, an `_folder.meta` a
10//! folder. The picture keeps its size: one dot a pixel.
11//!
12//! Each pixel is first a **brightness** from 0 (black) to 255 (white),
13//! `(77 r + 150 g + 29 b + 128) / 256`, turned over (`255 − b`) when the
14//! settings say [`invert`](OneBitSettings::invert); then the
15//! [`Dither`] makes it black or white. A pixel whose alpha is below
16//! [`alpha_cut`](OneBitSettings::alpha_cut) is see-through: clear in the
17//! mask (the file has one only if some pixel is), white in the picture,
18//! and no error spreads to or from it.
19//!
20//! [`Dithered`] (with the `png` feature) is the format that reads a PNG
21//! as a [`OneBitPicture`] this way.
22use 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/// How a picture's greys become black and white.
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Deserialize)]
40#[serde(rename_all = "snake_case")]
41#[non_exhaustive]
42pub enum Dither {
43    /// Atkinson's error diffusion, for pictures: each pixel is white from
44    /// a brightness of 128, and an eighth of how far that is from its
45    /// brightness goes to each of six pixels after it (the next two on
46    /// its row, three on the row below, one two rows down), so a grey
47    /// keeps its share of white. Three quarters of the error spread, so
48    /// light and dark areas keep their contrast.
49    #[default]
50    Atkinson,
51    /// The 8 × 8 Bayer ordered dither, for flat art and anything that
52    /// moves: a pixel is black where `4 × order + 2` is below its
53    /// darkness (`255 − brightness`), [`BAYER`]'s order at its place in
54    /// the picture, so a flat grey is the pattern
55    /// [`onebit::dither`](crate::onebit::dither) gives it.
56    Bayer,
57    /// White from this brightness up, black below it: for line art.
58    Threshold(u8),
59    /// No dither: every drawn pixel must already be black or white
60    /// (`#000000` or `#ffffff`), and any other is an error.
61    None,
62}
63
64/// A picture's `onebit` import settings section (ADR-0057): how it is
65/// made one-bit. Every field may be left out: `(onebit: ())` is the
66/// default, Atkinson with a cut at 128.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
68#[serde(default, deny_unknown_fields)]
69#[non_exhaustive]
70pub struct OneBitSettings {
71    /// How its greys become black and white.
72    pub dither: Dither,
73    /// The alpha below which a pixel is see-through (0 for none).
74    pub alpha_cut: u8,
75    /// Whether its brightness is turned over first, so black becomes
76    /// white.
77    pub invert: bool,
78}
79
80impl OneBitSettings {
81    /// The default settings, made one-bit by `dither`.
82    #[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
102/// A picture's brightness from 0 to 255 of the pixel `[r, g, b, _]`.
103pub 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    // At most (256 × 255 + 128) / 256: 255.
106    u8::try_from(sum >> 8).unwrap_or(u8::MAX)
107}
108
109/// The one-bit picture file of `image` made by `settings`: one style the
110/// image's size, and a mask if a pixel is see-through. Its source size is
111/// the image's own. An error, in plain words, for a picture too big for
112/// the file (a side over 65,535 pixels), with no pixels or short of them,
113/// or for one with a grey under [`Dither::None`].
114pub 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/// A PNG made a [`OneBitPicture`] by its `onebit` import settings
180/// (`dithered`, `.png`, with the `png` feature): its pixels read as
181/// [`Png`] reads them, then [`dither`]ed. A tool registers it; a one-bit
182/// device reads the pictures it made with
183/// [`OneBitFile`](super::OneBitFile).
184#[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
222/// Whether pixel `[r, g, b, _]` is pure black or pure white.
223fn is_black_or_white([r, g, b, _]: [u8; 4]) -> bool {
224    r == g && g == b && (r == 0 || r == 255)
225}
226
227/// What the dithers need of a picture: its width, and which pixels are
228/// drawn, row by row.
229struct Picture<'a> {
230    width: usize,
231    opaque: &'a [bool],
232}
233
234impl Picture<'_> {
235    /// Each pixel white where `white(brightness, x, y)` says; a
236    /// see-through one white.
237    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    /// Atkinson's error diffusion ([`Dither::Atkinson`]): each pixel in
246    /// order, row by row, white from 128, its error over 8 (rounded
247    /// toward 0) added to each drawn pixel of the six after it.
248    fn atkinson(&self, light: &[u8]) -> Vec<bool> {
249        /// Where the error goes, as `(dx, dy)`.
250        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        // Six eighths of each error spread: a level stays between about
253        // -400 and 700, well inside an `i16`.
254        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
280/// The rows of a plane `width` pixels wide, a pixel's bit set where
281/// `set` is.
282fn 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;