Skip to main content

oxihuman_export/
bmp_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! BMP image stub export.
6
7/// BMP bit depth.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum BmpBitDepth {
10    Bpp24,
11    Bpp32,
12}
13
14impl BmpBitDepth {
15    /// Bytes per pixel.
16    pub fn bytes_per_pixel(&self) -> usize {
17        match self {
18            Self::Bpp24 => 3,
19            Self::Bpp32 => 4,
20        }
21    }
22}
23
24/// BMP image stub.
25#[derive(Debug, Clone)]
26pub struct BmpExport {
27    pub width: u32,
28    pub height: u32,
29    pub bit_depth: BmpBitDepth,
30    pub pixels: Vec<[u8; 4]>,
31}
32
33impl BmpExport {
34    /// Create BMP filled with solid color.
35    pub fn new_solid(width: u32, height: u32, bit_depth: BmpBitDepth, color: [u8; 4]) -> Self {
36        let pixels = vec![color; (width * height) as usize];
37        Self {
38            width,
39            height,
40            bit_depth,
41            pixels,
42        }
43    }
44
45    /// Pixel count.
46    pub fn pixel_count(&self) -> usize {
47        self.pixels.len()
48    }
49}
50
51/// Build BMP header bytes (stub).
52pub fn build_bmp_header(export: &BmpExport) -> Vec<u8> {
53    let bytes_per_pixel = export.bit_depth.bytes_per_pixel();
54    let row_size = (export.width as usize * bytes_per_pixel).div_ceil(4) * 4;
55    let pixel_data_size = row_size * export.height as usize;
56    let file_size = 54 + pixel_data_size;
57    let mut header = vec![0u8; 54];
58    header[0] = b'B';
59    header[1] = b'M';
60    header[2..6].copy_from_slice(&(file_size as u32).to_le_bytes());
61    header[10] = 54;
62    header[14] = 40;
63    header[18..22].copy_from_slice(&export.width.to_le_bytes());
64    header[22..26].copy_from_slice(&export.height.to_le_bytes());
65    header[26] = 1;
66    header[28] = (bytes_per_pixel * 8) as u8;
67    header
68}
69
70/// Validate BMP export.
71pub fn validate_bmp(export: &BmpExport) -> bool {
72    export.width > 0
73        && export.height > 0
74        && export.pixels.len() == (export.width * export.height) as usize
75}
76
77/// Estimate BMP file size.
78pub fn estimate_bmp_bytes(export: &BmpExport) -> usize {
79    let bytes_per_pixel = export.bit_depth.bytes_per_pixel();
80    let row_size = (export.width as usize * bytes_per_pixel).div_ceil(4) * 4;
81    54 + row_size * export.height as usize
82}
83
84/// Average pixel brightness (0-255).
85pub fn average_brightness(export: &BmpExport) -> f32 {
86    if export.pixels.is_empty() {
87        return 0.0;
88    }
89    let sum: f32 = export
90        .pixels
91        .iter()
92        .map(|p| (p[0] as f32 + p[1] as f32 + p[2] as f32) / 3.0)
93        .sum();
94    sum / export.pixels.len() as f32
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn sample() -> BmpExport {
102        BmpExport::new_solid(8, 8, BmpBitDepth::Bpp24, [100, 150, 200, 255])
103    }
104
105    #[test]
106    fn test_pixel_count() {
107        /* pixel count matches dimensions */
108        assert_eq!(sample().pixel_count(), 64);
109    }
110
111    #[test]
112    fn test_validate_valid() {
113        /* valid export passes */
114        assert!(validate_bmp(&sample()));
115    }
116
117    #[test]
118    fn test_bmp_header_magic() {
119        /* BMP header starts with BM */
120        let h = build_bmp_header(&sample());
121        assert_eq!(h[0], b'B');
122        assert_eq!(h[1], b'M');
123    }
124
125    #[test]
126    fn test_estimate_bytes_positive() {
127        /* estimated size is positive */
128        assert!(estimate_bmp_bytes(&sample()) > 0);
129    }
130
131    #[test]
132    fn test_average_brightness() {
133        /* average brightness of grey pixel is correct */
134        let e = BmpExport::new_solid(2, 2, BmpBitDepth::Bpp24, [100, 100, 100, 255]);
135        assert!((average_brightness(&e) - 100.0).abs() < 1.0);
136    }
137
138    #[test]
139    fn test_bpp24_bytes_per_pixel() {
140        /* 24-bit has 3 bytes per pixel */
141        assert_eq!(BmpBitDepth::Bpp24.bytes_per_pixel(), 3);
142    }
143
144    #[test]
145    fn test_bpp32_estimate_larger() {
146        /* 32-bit estimate is larger than 24-bit */
147        let e24 = BmpExport::new_solid(8, 8, BmpBitDepth::Bpp24, [0; 4]);
148        let e32 = BmpExport::new_solid(8, 8, BmpBitDepth::Bpp32, [0; 4]);
149        assert!(estimate_bmp_bytes(&e32) > estimate_bmp_bytes(&e24));
150    }
151
152    #[test]
153    fn test_validate_zero_dim() {
154        /* zero dimension fails validation */
155        let mut e = sample();
156        e.width = 0;
157        assert!(!validate_bmp(&e));
158    }
159}