Skip to main content

patterns/
capture.rs

1use patterns_macros::{because, source};
2
3pub struct BitmapFileFormat;
4source!(
5    BitmapFileFormat,
6    "the Windows bitmap file format, its outer and inner headers and the order of their fields"
7);
8
9pub const CHANNELS: usize = 3;
10because!(CHANNELS, BitmapFileFormat, "the bytes one pixel takes in a saved image, one for each of blue green and red, since a picture of a screen keeps colour and has no use for transparency");
11
12pub const CHANNELS_WITH_ALPHA: usize = 4;
13because!(CHANNELS_WITH_ALPHA, BitmapFileFormat, "the bytes one pixel takes as a graphics device hands it back, the three colours and an alpha that means nothing once the frame is flat");
14
15const ROW_BLOCK: usize = 4;
16because!(ROW_BLOCK, BitmapFileFormat, "the byte multiple every row of a bitmap is padded up to, which the format demands and which is the reason a saved frame comes out sheared when a writer forgets it");
17
18const FILE_HEADER: u32 = 14;
19because!(FILE_HEADER, BitmapFileFormat, "the bytes of the outer header, holding the signature, the length of the whole file and the offset the pixels start at");
20
21const INFO_HEADER: u32 = 40;
22because!(INFO_HEADER, BitmapFileFormat, "the bytes of the inner header, holding the width, the height, the plane count, the depth and the compression; the oldest of several sizes the format allows and the one every viewer reads");
23
24const PIXELS_AT: u32 = FILE_HEADER + INFO_HEADER;
25
26const SIGNATURE_FIRST: u8 = 66;
27because!(SIGNATURE_FIRST, BitmapFileFormat, "the letter B that opens every bitmap, written as its code point because a byte string is a literal and a literal here would state the fact a second time");
28
29const SIGNATURE_SECOND: u8 = 77;
30because!(SIGNATURE_SECOND, BitmapFileFormat, "the letter M that completes the two byte signature a bitmap opens with, written as its code point because a byte string is a literal and one here would state the fact a second time");
31
32const PLANES: u16 = 1;
33because!(PLANES, BitmapFileFormat, "the single plane a bitmap has, a field the format kept from a time when a device stored each colour separately, and which every reader still expects to find");
34
35const DEPTH: u16 = 24;
36because!(DEPTH, BitmapFileFormat, "the bits one pixel takes in the file, eight for each colour, the one depth that needs no palette and that every viewer opens");
37
38fn padded_row(wide: usize) -> usize {
39    let bare = wide * CHANNELS;
40    bare + (ROW_BLOCK - bare % ROW_BLOCK) % ROW_BLOCK
41}
42because!(padded_row, "the bytes one row of the saved image occupies once it is padded, kept apart from the writing so the size in the header and the padding after each row cannot disagree");
43
44pub fn without_alpha(pixels: &[u8]) -> Vec<u8> {
45    pixels
46        .chunks(CHANNELS_WITH_ALPHA)
47        .flat_map(|p| p.iter().take(CHANNELS).copied())
48        .collect()
49}
50because!(without_alpha, "a frame from a device narrowed to the colours a file keeps, so a caller hands over what it was given rather than counting bytes at the call");
51
52pub fn rows_flipped(pixels: &[u8], stride: usize) -> Vec<u8> {
53    if stride == 0 {
54        return Vec::new();
55    }
56    pixels.chunks(stride).rev().flatten().copied().collect()
57}
58because!(rows_flipped, "a frame turned over, for a device whose first row is the bottom of the screen rather than the top, which is the difference between the two conventions and nothing else");
59
60pub fn bitmap(pixels: &[u8], wide: usize, high: usize) -> Vec<u8> {
61    let row = padded_row(wide);
62    let body = row * high;
63    let size = PIXELS_AT as usize + body;
64    let mut out: Vec<u8> = Vec::with_capacity(size);
65    out.push(SIGNATURE_FIRST);
66    out.push(SIGNATURE_SECOND);
67    out.extend_from_slice(&(size as u32).to_le_bytes());
68    out.extend_from_slice(&0u16.to_le_bytes());
69    out.extend_from_slice(&0u16.to_le_bytes());
70    out.extend_from_slice(&PIXELS_AT.to_le_bytes());
71    out.extend_from_slice(&INFO_HEADER.to_le_bytes());
72    out.extend_from_slice(&(wide as i32).to_le_bytes());
73    out.extend_from_slice(&(high as i32).to_le_bytes());
74    out.extend_from_slice(&PLANES.to_le_bytes());
75    out.extend_from_slice(&DEPTH.to_le_bytes());
76    out.extend_from_slice(&0u32.to_le_bytes());
77    out.extend_from_slice(&(body as u32).to_le_bytes());
78    out.extend_from_slice(&0i32.to_le_bytes());
79    out.extend_from_slice(&0i32.to_le_bytes());
80    out.extend_from_slice(&0u32.to_le_bytes());
81    out.extend_from_slice(&0u32.to_le_bytes());
82    let line = wide * CHANNELS;
83    for y in (0..high).rev() {
84        let from = y * line;
85        let taken = pixels.get(from..from + line).unwrap_or(&[]);
86        for pixel in taken.chunks(CHANNELS) {
87            out.extend(pixel.iter().rev().copied());
88        }
89        out.resize(out.len() + row.saturating_sub(taken.len()), 0);
90    }
91    out
92}
93because!(bitmap, "a frame of colours as the bytes of a file any desktop opens, holding in one place the row padding, the bottom upward row order and the blue before red byte order that a viewer expects and that nothing about the frame itself would tell you");
94
95pub fn saved(path: &str, bytes: &[u8]) -> Result<(), String> {
96    std::fs::write(path, bytes).map_err(|e| e.to_string())
97}
98because!(saved, "bytes put on the disk, the one place the library touches a file, so a binding that wants to keep something says where and not how, and a failure comes back as a value rather than ending the program");
99
100pub fn shot(path: &str, pixels: &[u8], wide: usize, high: usize) -> Result<(), String> {
101    saved(path, &bitmap(pixels, wide, high))
102}
103because!(shot, "a frame written to a file in one call, the whole of what a binding needs to keep a picture of what it drew, so the two halves are never composed wrongly at the call");