Skip to main content

shuyi_assets/
bitmap.rs

1//! One-bit pictures (game ADR-0085): the file a one-bit screen draws in place of
2//! a PNG, made ahead of time by [`convert`] so the device never decodes a
3//! PNG. Pixels are packed one bit a pixel in rows, the Playdate's own
4//! bitmap layout: each row [`stride`](Bitmap::stride) bytes, a byte's
5//! highest bit its left pixel, a set bit white.
6//!
7//! A file holds one picture in one or more **styles** (the same picture
8//! converted different ways, all the same size) and at most one **mask**
9//! they share: a set bit where the picture is drawn, a clear one where it
10//! is see-through.
11//!
12//! A picture is usually bigger than the PNG it was made from: it is
13//! enlarged to the size it is shown at before it is dithered, so each dot
14//! is one screen pixel. The file keeps the PNG's own size too (the
15//! **source size**), so a game keeps naming parts of the picture in the
16//! PNG's pixels and a renderer scales them to the picture's.
17//!
18//! The layout, all numbers little-endian:
19//!
20//! | Bytes | What |
21//! | ----- | ---- |
22//! | 0–3 | [`MAGIC`], `1BIT` |
23//! | 4 | [`VERSION`] |
24//! | 5 | flags: bit 0 set if there is a mask; the others clear |
25//! | 6 | the number of styles, at least 1 |
26//! | 7 | 0 |
27//! | 8–9, 10–11 | width and height in pixels, each at least 1 |
28//! | 12–13, 14–15 | the source size, each at least 1 |
29//! | 16… | each style's rows, in order, then the mask's rows if any |
30//!
31//! As formats (ADR-0007), a one-bit picture in a PNG's place is read for
32//! the size of that PNG ([`OneBitInfo`]), and whole as a
33//! [`OneBitPicture`] ([`OneBitFile`]). A one-bit target's pack holds its
34//! pictures so: `shuyi bake --target playdate` makes each PNG one in
35//! integers ([`dither`]), and the device draws it with no copying.
36use crate::prelude::*;
37
38use serde::Serialize;
39
40use crate::error::ContentError;
41use crate::format::{AssetKind, AssetPath, DecodeCx, Encode, Format};
42use crate::image::ImageInfo;
43
44#[cfg(feature = "std")]
45pub mod convert;
46pub mod dither;
47
48/// The most bytes a one-bit picture file may have: 16 MiB, far more than
49/// a one-bit screen's pictures need (the Playdate's whole screen is 12 KB
50/// a style).
51pub const MAX_BYTES: usize = 16 << 20;
52
53/// A one-bit picture in a PNG's place, read for the size of the PNG it
54/// was made from (`one-bit`), which [`ImageInfo::checked`] must allow. It
55/// claims no extension: it is found by its magic bytes at a PNG's path,
56/// so a `.1bit` file beside a PNG (a touched-up picture a build reads) is
57/// not an image of its own.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59pub struct OneBitInfo;
60
61impl Format for OneBitInfo {
62    type Asset = ImageInfo;
63
64    fn id(&self) -> &'static str {
65        "one-bit"
66    }
67
68    fn extensions(&self) -> &'static [&'static str] {
69        &[]
70    }
71
72    fn sniff(&self, bytes: &[u8]) -> bool {
73        Bitmap::is_bitmap(bytes)
74    }
75
76    fn max_bytes(&self) -> usize {
77        MAX_BYTES
78    }
79
80    fn decode(
81        &self,
82        file: &AssetPath,
83        bytes: &[u8],
84        _cx: &DecodeCx<'_>,
85    ) -> Result<ImageInfo, Vec<ContentError>> {
86        source_info(bytes).map_err(|message| vec![ContentError::new(file.shown(), message)])
87    }
88}
89
90/// A one-bit picture file, whole and checked ([`Bitmap::parse`]): what a
91/// one-bit target's pictures are baked as (ADR-0008 §4). [`OneBitFile`]
92/// reads one from a one-bit picture file, and with the `png` feature
93/// [`dither::Dithered`] makes one from a PNG.
94#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
95pub struct OneBitPicture {
96    file: Vec<u8>,
97}
98
99impl OneBitPicture {
100    /// The picture of one-bit picture file `file`, or what is wrong with
101    /// it ([`Bitmap::parse`]).
102    pub fn new(file: Vec<u8>) -> Result<Self, &'static str> {
103        Bitmap::parse(&file)?;
104        Ok(Self { file })
105    }
106
107    /// Its file's bytes, which [`Bitmap::parse`] reads.
108    pub fn file(&self) -> &[u8] {
109        &self.file
110    }
111
112    /// Its file's bytes, kept.
113    pub fn into_file(self) -> Vec<u8> {
114        self.file
115    }
116}
117
118impl AssetKind for OneBitPicture {
119    const NAME: &'static str = "one-bit picture";
120}
121
122/// A one-bit picture file read whole as a [`OneBitPicture`] (`one-bit`).
123/// Like [`OneBitInfo`], it claims no extension and is found by its magic
124/// bytes, at a PNG's path in a one-bit target's pack or in place of a PNG
125/// in a target's layer. It writes the file back as it was ([`Encode`]),
126/// which is how a bake keeps a picture in its pack
127/// ([`Bake::under_as`](crate::pack::Bake::under_as)).
128#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
129pub struct OneBitFile;
130
131impl Format for OneBitFile {
132    type Asset = OneBitPicture;
133
134    fn id(&self) -> &'static str {
135        "one-bit"
136    }
137
138    fn extensions(&self) -> &'static [&'static str] {
139        &[]
140    }
141
142    fn sniff(&self, bytes: &[u8]) -> bool {
143        Bitmap::is_bitmap(bytes)
144    }
145
146    fn max_bytes(&self) -> usize {
147        MAX_BYTES
148    }
149
150    fn decode(
151        &self,
152        file: &AssetPath,
153        bytes: &[u8],
154        _cx: &DecodeCx<'_>,
155    ) -> Result<OneBitPicture, Vec<ContentError>> {
156        OneBitPicture::new(bytes.to_vec())
157            .map_err(|message| vec![ContentError::new(file.shown(), message)])
158    }
159}
160
161impl Encode for OneBitFile {
162    fn encode(&self, asset: &OneBitPicture) -> Vec<u8> {
163        asset.file.clone()
164    }
165}
166
167/// The checked source size of one-bit picture file `bytes`, or what is
168/// wrong with it.
169pub(crate) fn source_info(bytes: &[u8]) -> Result<ImageInfo, String> {
170    let (width, height) = Bitmap::parse(bytes)?.source_size();
171    ImageInfo::checked(u32::from(width), u32::from(height))
172}
173
174/// The first four bytes of a one-bit picture file.
175pub const MAGIC: [u8; 4] = *b"1BIT";
176
177/// The layout's version: the fifth byte.
178pub const VERSION: u8 = 1;
179
180/// Bytes before the first style's rows.
181pub const HEADER_BYTES: usize = 16;
182
183/// The extension of a one-bit picture file on disk (`.1bit`).
184pub const EXTENSION: &str = "1bit";
185
186/// Flag bit: the file has a mask.
187const HAS_MASK: u8 = 1;
188
189/// A one-bit picture file, read without copying ([`Bitmap::parse`]).
190#[derive(Copy, Clone, Debug, PartialEq, Eq)]
191pub struct Bitmap<'a> {
192    width: u16,
193    height: u16,
194    source_width: u16,
195    source_height: u16,
196    styles: u8,
197    /// Every style's rows, one after the other.
198    planes: &'a [u8],
199    mask: Option<&'a [u8]>,
200}
201
202impl<'a> Bitmap<'a> {
203    /// Whether `bytes` starts as a one-bit picture file does.
204    pub fn is_bitmap(bytes: &[u8]) -> bool {
205        bytes.starts_with(&MAGIC)
206    }
207
208    /// Reads the file `bytes`, or says in plain words what is wrong with it.
209    pub fn parse(bytes: &'a [u8]) -> Result<Self, &'static str> {
210        let header = bytes
211            .get(..HEADER_BYTES)
212            .ok_or("too short for a one-bit picture's header")?;
213        if !Self::is_bitmap(header) {
214            return Err("not a one-bit picture file");
215        }
216        if header[4] != VERSION {
217            return Err("a one-bit picture of an unknown version");
218        }
219        let flags = header[5];
220        if flags & !HAS_MASK != 0 || header[7] != 0 {
221            return Err("a one-bit picture with unknown flags");
222        }
223        let styles = header[6];
224        let word = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
225        let (width, height) = (word(8), word(10));
226        let (source_width, source_height) = (word(12), word(14));
227        if styles == 0 || width == 0 || height == 0 || source_width == 0 || source_height == 0 {
228            return Err("a one-bit picture with no styles or no pixels");
229        }
230        let plane = Self::plane_bytes(width, height);
231        let has_mask = flags & HAS_MASK != 0;
232        let body = &bytes[HEADER_BYTES..];
233        // Checked: on a 32-bit target (the web) 255 styles of the largest
234        // plane don't fit a `usize`.
235        let mask_bytes = if has_mask { plane } else { 0 };
236        let Some(planes_bytes) = plane
237            .checked_mul(usize::from(styles))
238            .filter(|&n| n.checked_add(mask_bytes) == Some(body.len()))
239        else {
240            return Err("a one-bit picture's size doesn't match its header");
241        };
242        let (planes, mask) = body.split_at(planes_bytes);
243        Ok(Self {
244            width,
245            height,
246            source_width,
247            source_height,
248            styles,
249            planes,
250            mask: has_mask.then_some(mask),
251        })
252    }
253
254    /// Bytes per row of a picture `width` pixels wide.
255    pub const fn stride(width: u16) -> usize {
256        (width as usize).div_ceil(8)
257    }
258
259    /// Bytes of one style (or of the mask) of a `width` × `height` picture.
260    pub const fn plane_bytes(width: u16, height: u16) -> usize {
261        Self::stride(width) * height as usize
262    }
263
264    /// Width in pixels.
265    pub const fn width(&self) -> u16 {
266        self.width
267    }
268
269    /// Height in pixels.
270    pub const fn height(&self) -> u16 {
271        self.height
272    }
273
274    /// The size of the PNG it was made from, in that PNG's pixels.
275    pub const fn source_size(&self) -> (u16, u16) {
276        (self.source_width, self.source_height)
277    }
278
279    /// How many styles it holds.
280    pub const fn styles(&self) -> usize {
281        self.styles as usize
282    }
283
284    /// The rows of style `n` (0 is the first), or `None` past the last.
285    pub fn style(&self, n: usize) -> Option<&'a [u8]> {
286        let plane = Self::plane_bytes(self.width, self.height);
287        (n < self.styles()).then(|| &self.planes[n * plane..(n + 1) * plane])
288    }
289
290    /// The mask's rows, if it has one.
291    pub const fn mask(&self) -> Option<&'a [u8]> {
292        self.mask
293    }
294
295    /// Pixel `(x, y)` of style `n`: `Some(true)` white, `Some(false)`
296    /// black, `None` see-through, off the picture or past the last style.
297    pub fn pixel(&self, n: usize, x: u16, y: u16) -> Option<bool> {
298        if x >= self.width || y >= self.height {
299            return None;
300        }
301        let at = usize::from(y) * Self::stride(self.width) + usize::from(x / 8);
302        let bit = |rows: &[u8]| rows[at] >> (7 - x % 8) & 1 == 1;
303        let rows = self.style(n)?;
304        match self.mask {
305            Some(mask) if !bit(mask) => None,
306            _ => Some(bit(rows)),
307        }
308    }
309}
310
311/// The size, in pixels, of one-bit picture file `bytes`, and its source
312/// size, or `None` if it doesn't parse ([`Bitmap::parse`]).
313pub fn bitmap_sizes(bytes: &[u8]) -> Option<((u16, u16), (u16, u16))> {
314    let bitmap = Bitmap::parse(bytes).ok()?;
315    Some(((bitmap.width, bitmap.height), bitmap.source_size()))
316}
317
318/// The file of a `width` × `height` picture made from a PNG of
319/// `source` size: `styles` (each [`Bitmap::plane_bytes`] long) and the
320/// `mask` they share, or what is wrong with them.
321pub fn encode(
322    (width, height): (u16, u16),
323    source: (u16, u16),
324    styles: &[&[u8]],
325    mask: Option<&[u8]>,
326) -> Result<Vec<u8>, &'static str> {
327    let plane = Bitmap::plane_bytes(width, height);
328    let count = u8::try_from(styles.len()).map_err(|_| "more than 255 styles")?;
329    if count == 0 || width == 0 || height == 0 || source.0 == 0 || source.1 == 0 {
330        return Err("a one-bit picture needs a style and pixels");
331    }
332    if styles
333        .iter()
334        .chain(mask.as_ref())
335        .any(|rows| rows.len() != plane)
336    {
337        return Err("a style or the mask isn't the picture's size");
338    }
339    let mut out = Vec::with_capacity(HEADER_BYTES + plane * (styles.len() + 1));
340    out.extend(MAGIC);
341    out.extend([VERSION, if mask.is_some() { HAS_MASK } else { 0 }, count, 0]);
342    for word in [width, height, source.0, source.1] {
343        out.extend(word.to_le_bytes());
344    }
345    for rows in styles.iter().chain(mask.as_ref()) {
346        out.extend_from_slice(rows);
347    }
348    Ok(out)
349}
350
351#[cfg(test)]
352pub(crate) mod tests {
353    use super::*;
354    use proptest::prelude::*;
355
356    /// A 9 × 2 picture made from a 3 × 1 PNG: two styles and a mask.
357    pub(crate) fn sample() -> Vec<u8> {
358        let a: &[u8] = &[0b1000_0000, 0b1000_0000, 0, 0];
359        let b: &[u8] = &[0b0111_1111, 0, 0xff, 0x80];
360        let mask: &[u8] = &[0xff, 0x80, 0b0111_1111, 0];
361        encode((9, 2), (3, 1), &[a, b], Some(mask)).unwrap()
362    }
363
364    #[test]
365    fn the_header_is_as_documented() {
366        let file = sample();
367        assert_eq!(&file[..4], b"1BIT");
368        assert_eq!(file[4..8], [1, 1, 2, 0]);
369        assert_eq!(file[8..16], [9, 0, 2, 0, 3, 0, 1, 0]);
370        assert_eq!(file.len(), HEADER_BYTES + 3 * 4);
371        let plain = encode((8, 1), (8, 1), &[&[0x0f]], None).unwrap();
372        assert_eq!(plain[5], 0);
373        assert_eq!(plain[16..], [0x0f]);
374    }
375
376    #[test]
377    fn sizes_past_255_read_both_bytes() {
378        let plane = vec![0u8; Bitmap::plane_bytes(300, 1)];
379        let file = encode((300, 1), (258, 513), &[&plane], None).unwrap();
380        assert_eq!(file[8..16], [0x2c, 1, 1, 0, 2, 1, 1, 2]);
381        let bitmap = Bitmap::parse(&file).unwrap();
382        assert_eq!((bitmap.width(), bitmap.height()), (300, 1));
383        assert_eq!(bitmap.source_size(), (258, 513));
384    }
385
386    #[test]
387    fn a_file_reads_back_as_written() {
388        let file = sample();
389        let bitmap = Bitmap::parse(&file).unwrap();
390        assert_eq!((bitmap.width(), bitmap.height()), (9, 2));
391        assert_eq!(bitmap.source_size(), (3, 1));
392        assert_eq!(bitmap.styles(), 2);
393        assert_eq!(bitmap.style(0), Some(&[0x80, 0x80, 0, 0][..]));
394        assert_eq!(bitmap.style(1), Some(&[0x7f, 0, 0xff, 0x80][..]));
395        assert_eq!(bitmap.style(2), None);
396        assert_eq!(bitmap.mask(), Some(&[0xff, 0x80, 0x7f, 0][..]));
397        assert_eq!(bitmap_sizes(&file), Some(((9, 2), (3, 1))));
398        assert!(Bitmap::is_bitmap(&file));
399        assert!(!Bitmap::is_bitmap(b"\x89PNG"));
400    }
401
402    #[test]
403    fn pixels_are_read_highest_bit_first_and_masked() {
404        let file = sample();
405        let bitmap = Bitmap::parse(&file).unwrap();
406        assert_eq!(bitmap.pixel(0, 0, 0), Some(true));
407        assert_eq!(bitmap.pixel(0, 1, 0), Some(false));
408        assert_eq!(bitmap.pixel(0, 8, 0), Some(true));
409        assert_eq!(bitmap.pixel(1, 0, 0), Some(false));
410        assert_eq!(bitmap.pixel(1, 1, 0), Some(true));
411        // The mask's first pixel of the second row is clear: see-through.
412        assert_eq!(bitmap.pixel(1, 0, 1), None);
413        assert_eq!(bitmap.pixel(1, 1, 1), Some(true));
414        assert_eq!(bitmap.pixel(1, 8, 1), None);
415        assert_eq!(bitmap.pixel(0, 9, 0), None);
416        assert_eq!(bitmap.pixel(0, 0, 2), None);
417        assert_eq!(bitmap.pixel(2, 0, 0), None);
418    }
419
420    #[test]
421    fn bad_files_say_what_is_wrong() {
422        let file = sample();
423        let error = |bytes: &[u8]| Bitmap::parse(bytes).err().unwrap_or_default();
424        assert_eq!(
425            error(&file[..15]),
426            "too short for a one-bit picture's header"
427        );
428        let mut png = file.clone();
429        png[..4].copy_from_slice(b"\x89PNG");
430        assert_eq!(error(&png), "not a one-bit picture file");
431        for (at, value, message) in [
432            (4, 2, "a one-bit picture of an unknown version"),
433            (5, 3, "a one-bit picture with unknown flags"),
434            (7, 1, "a one-bit picture with unknown flags"),
435            (6, 0, "a one-bit picture with no styles or no pixels"),
436            (8, 0, "a one-bit picture with no styles or no pixels"),
437            (10, 0, "a one-bit picture with no styles or no pixels"),
438            (12, 0, "a one-bit picture with no styles or no pixels"),
439            (14, 0, "a one-bit picture with no styles or no pixels"),
440            (6, 3, "a one-bit picture's size doesn't match its header"),
441            (5, 0, "a one-bit picture's size doesn't match its header"),
442        ] {
443            let mut bad = file.clone();
444            bad[at] = value;
445            if at == 8 || at == 10 || at == 12 || at == 14 {
446                bad[at + 1] = 0;
447            }
448            assert_eq!(error(&bad), message, "byte {at} = {value}");
449        }
450        let mut long = file.clone();
451        long.push(0);
452        assert_eq!(
453            error(&long),
454            "a one-bit picture's size doesn't match its header"
455        );
456        assert_eq!(bitmap_sizes(&long), None);
457    }
458
459    #[test]
460    fn encode_refuses_what_it_cannot_write() {
461        let row: &[u8] = &[0];
462        assert!(encode((8, 1), (8, 1), &[], None).is_err());
463        assert!(encode((0, 1), (8, 1), &[&[]], None).is_err());
464        assert!(encode((8, 0), (8, 1), &[&[]], None).is_err());
465        assert!(encode((8, 1), (0, 1), &[row], None).is_err());
466        assert!(encode((8, 1), (8, 0), &[row], None).is_err());
467        assert!(encode((8, 1), (8, 1), &[&[0, 0]], None).is_err());
468        assert!(encode((8, 1), (8, 1), &[row], Some(&[])).is_err());
469        assert!(encode((8, 1), (8, 1), &[row; 256], None).is_err());
470        assert!(encode((8, 1), (8, 1), &[row; 255], None).is_ok());
471    }
472
473    proptest! {
474        /// Any picture written reads back with the same pixels.
475        #[test]
476        fn any_picture_round_trips(
477            width in 1u16..20,
478            height in 1u16..6,
479            source in (1u16..30, 1u16..30),
480            styles in 1usize..4,
481            masked: bool,
482            seed: u64,
483        ) {
484            let plane = Bitmap::plane_bytes(width, height);
485            let byte = |n: usize| seed.rotate_left(u32::try_from(n % 64).unwrap() * 7).to_le_bytes()[0] ^ n.to_le_bytes()[0];
486            let planes: Vec<Vec<u8>> = (0..=styles)
487                .map(|s| (0..plane).map(|i| byte(s * plane + i)).collect())
488                .collect();
489            let refs: Vec<&[u8]> = planes[..styles].iter().map(Vec::as_slice).collect();
490            let mask = masked.then_some(planes[styles].as_slice());
491            let file = encode((width, height), source, &refs, mask).unwrap();
492            let bitmap = Bitmap::parse(&file).unwrap();
493            prop_assert_eq!(bitmap.source_size(), source);
494            for (n, rows) in refs.iter().enumerate() {
495                prop_assert_eq!(bitmap.style(n), Some(*rows));
496            }
497            prop_assert_eq!(bitmap.mask(), mask);
498        }
499    }
500}