hwpforge_foundation/
color.rs

1//! BGR color type matching the HWP specification.
2//!
3//! HWP documents store colors in **BGR** byte order (blue in the high byte,
4//! red in the low byte). This module provides [`Color`], a zero-cost `u32`
5//! wrapper that makes BGR handling explicit and safe.
6//!
7//! Bits 24-31 are reserved (typically zero). The HWP5 format uses bit 24
8//! as a transparency flag; this crate treats those bits as opaque data
9//! preserved through round-trips.
10//!
11//! # Examples
12//!
13//! ```
14//! use hwpforge_foundation::Color;
15//!
16//! let red = Color::from_rgb(255, 0, 0);
17//! assert_eq!(red.red(), 255);
18//! assert_eq!(red.green(), 0);
19//! assert_eq!(red.blue(), 0);
20//! assert_eq!(red.to_raw(), 0x000000FF); // BGR: red in low byte
21//! ```
22
23use std::fmt;
24
25use serde::{Deserialize, Serialize};
26
27/// A color stored in BGR format, matching the HWP binary specification.
28///
29/// Internally a `u32` with `repr(transparent)` for zero overhead.
30///
31/// FROZEN: Do not change the internal representation after v1.0.
32///
33/// # Layout
34///
35/// ```text
36/// Bits: [31..24 reserved] [23..16 blue] [15..8 green] [7..0 red]
37/// ```
38///
39/// # Examples
40///
41/// ```
42/// use hwpforge_foundation::Color;
43///
44/// let c = Color::from_rgb(0x11, 0x22, 0x33);
45/// assert_eq!(c.to_raw(), 0x00332211);
46/// assert_eq!(c.to_string(), "#112233");
47/// ```
48#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[repr(transparent)]
50pub struct Color(u32);
51
52// Compile-time size guarantee
53const _: () = assert!(std::mem::size_of::<Color>() == 4);
54
55impl Color {
56    // Named constants (all with reserved bits = 0)
57
58    /// Black: RGB(0, 0, 0).
59    pub const BLACK: Self = Self(0x00000000);
60    /// White: RGB(255, 255, 255).
61    pub const WHITE: Self = Self(0x00FFFFFF);
62    /// Pure red: RGB(255, 0, 0).
63    pub const RED: Self = Self(0x000000FF);
64    /// Pure green: RGB(0, 255, 0).
65    pub const GREEN: Self = Self(0x0000FF00);
66    /// Pure blue: RGB(0, 0, 255).
67    pub const BLUE: Self = Self(0x00FF0000);
68
69    /// Constructs a [`Color`] from RGB components.
70    ///
71    /// The components are stored in BGR order internally.
72    /// Reserved bits (24-31) are set to zero.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use hwpforge_foundation::Color;
78    ///
79    /// let magenta = Color::from_rgb(255, 0, 255);
80    /// assert_eq!(magenta.red(), 255);
81    /// assert_eq!(magenta.blue(), 255);
82    /// ```
83    pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
84        Self((b as u32) << 16 | (g as u32) << 8 | (r as u32))
85    }
86
87    /// Extracts the RGB components as a tuple `(r, g, b)`.
88    pub const fn to_rgb(self) -> (u8, u8, u8) {
89        (self.red(), self.green(), self.blue())
90    }
91
92    /// Constructs a [`Color`] from a raw BGR `u32`.
93    ///
94    /// Use this when reading values directly from HWP binary data.
95    /// All 32 bits are preserved (including reserved bits 24-31).
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use hwpforge_foundation::Color;
101    ///
102    /// let c = Color::from_raw(0x00FF0000);
103    /// assert_eq!(c.blue(), 255);
104    /// assert_eq!(c.red(), 0);
105    /// ```
106    pub const fn from_raw(bgr: u32) -> Self {
107        Self(bgr)
108    }
109
110    /// Returns the raw BGR `u32` value.
111    pub const fn to_raw(self) -> u32 {
112        self.0
113    }
114
115    /// Returns the red component (bits 0-7).
116    pub const fn red(self) -> u8 {
117        (self.0 & 0xFF) as u8
118    }
119
120    /// Returns the green component (bits 8-15).
121    pub const fn green(self) -> u8 {
122        ((self.0 >> 8) & 0xFF) as u8
123    }
124
125    /// Returns the blue component (bits 16-23).
126    pub const fn blue(self) -> u8 {
127        ((self.0 >> 16) & 0xFF) as u8
128    }
129
130    /// Converts this color to an RGB hex string in `#RRGGBB` format.
131    ///
132    /// Since HWP uses BGR byte order internally, this method swaps the bytes
133    /// to produce standard RGB hex output.
134    ///
135    /// # Examples
136    /// ```
137    /// use hwpforge_foundation::Color;
138    /// let red = Color::from_rgb(255, 0, 0);
139    /// assert_eq!(red.to_hex_rgb(), "#FF0000");
140    /// ```
141    #[allow(clippy::wrong_self_convention)]
142    pub fn to_hex_rgb(&self) -> String {
143        format!("#{:02X}{:02X}{:02X}", self.red(), self.green(), self.blue())
144    }
145}
146
147impl Default for Color {
148    fn default() -> Self {
149        Self::BLACK
150    }
151}
152
153impl fmt::Debug for Color {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "Color(#{:02X}{:02X}{:02X})", self.red(), self.green(), self.blue())
156    }
157}
158
159impl fmt::Display for Color {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        write!(f, "#{:02X}{:02X}{:02X}", self.red(), self.green(), self.blue())
162    }
163}
164
165impl schemars::JsonSchema for Color {
166    fn schema_name() -> std::borrow::Cow<'static, str> {
167        "Color".into()
168    }
169
170    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
171        gen.subschema_for::<u32>()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    // ===================================================================
180    // Color edge cases (10+)
181    // ===================================================================
182
183    // Edge Case 1: Pure red -- BGR byte order check
184    #[test]
185    fn color_pure_red_bgr_order() {
186        let c = Color::from_rgb(255, 0, 0);
187        assert_eq!(c.to_raw(), 0x000000FF);
188        assert_eq!(c.red(), 255);
189        assert_eq!(c.green(), 0);
190        assert_eq!(c.blue(), 0);
191    }
192
193    // Edge Case 2: Pure blue -- high byte in BGR
194    #[test]
195    fn color_pure_blue_bgr_order() {
196        let c = Color::from_rgb(0, 0, 255);
197        assert_eq!(c.to_raw(), 0x00FF0000);
198        assert_eq!(c.blue(), 255);
199        assert_eq!(c.red(), 0);
200    }
201
202    // Edge Case 3: Black
203    #[test]
204    fn color_black() {
205        let c = Color::from_rgb(0, 0, 0);
206        assert_eq!(c, Color::BLACK);
207        assert_eq!(c.to_raw(), 0x00000000);
208    }
209
210    // Edge Case 4: White
211    #[test]
212    fn color_white() {
213        let c = Color::from_rgb(255, 255, 255);
214        assert_eq!(c, Color::WHITE);
215        assert_eq!(c.to_raw(), 0x00FFFFFF);
216    }
217
218    // Edge Case 5: RGB -> BGR -> RGB roundtrip
219    #[test]
220    fn color_rgb_roundtrip() {
221        let (r, g, b) = (0x11, 0x22, 0x33);
222        let c = Color::from_rgb(r, g, b);
223        assert_eq!(c.to_rgb(), (r, g, b));
224    }
225
226    // Edge Case 6: Raw u32::MAX -- reserved bits preserved
227    #[test]
228    fn color_from_raw_u32_max() {
229        let c = Color::from_raw(u32::MAX);
230        assert_eq!(c.red(), 255);
231        assert_eq!(c.green(), 255);
232        assert_eq!(c.blue(), 255);
233        assert_eq!(c.to_raw(), u32::MAX);
234    }
235
236    // Edge Case 7: Raw 0 == BLACK
237    #[test]
238    fn color_from_raw_zero() {
239        let c = Color::from_raw(0);
240        assert_eq!(c, Color::BLACK);
241    }
242
243    // Edge Case 8: Named constants match from_rgb
244    #[test]
245    fn color_named_constants() {
246        assert_eq!(Color::RED, Color::from_rgb(255, 0, 0));
247        assert_eq!(Color::GREEN, Color::from_rgb(0, 255, 0));
248        assert_eq!(Color::BLUE, Color::from_rgb(0, 0, 255));
249        assert_eq!(Color::BLACK, Color::from_rgb(0, 0, 0));
250        assert_eq!(Color::WHITE, Color::from_rgb(255, 255, 255));
251    }
252
253    // Edge Case 9: Display as #RRGGBB
254    #[test]
255    fn color_display_hex() {
256        let c = Color::from_rgb(0xAB, 0xCD, 0xEF);
257        assert_eq!(c.to_string(), "#ABCDEF");
258    }
259
260    // Edge Case 10: Default is BLACK
261    #[test]
262    fn color_default_is_black() {
263        assert_eq!(Color::default(), Color::BLACK);
264    }
265
266    // Additional tests
267
268    #[test]
269    fn color_to_hex_rgb_red() {
270        assert_eq!(Color::from_rgb(255, 0, 0).to_hex_rgb(), "#FF0000");
271    }
272
273    #[test]
274    fn color_to_hex_rgb_green() {
275        assert_eq!(Color::from_rgb(0, 255, 0).to_hex_rgb(), "#00FF00");
276    }
277
278    #[test]
279    fn color_to_hex_rgb_blue() {
280        assert_eq!(Color::from_rgb(0, 0, 255).to_hex_rgb(), "#0000FF");
281    }
282
283    #[test]
284    fn color_to_hex_rgb_black() {
285        assert_eq!(Color::from_rgb(0, 0, 0).to_hex_rgb(), "#000000");
286    }
287
288    #[test]
289    fn color_to_hex_rgb_white() {
290        assert_eq!(Color::from_rgb(255, 255, 255).to_hex_rgb(), "#FFFFFF");
291    }
292
293    #[test]
294    fn color_to_hex_rgb_arbitrary() {
295        assert_eq!(Color::from_rgb(18, 52, 86).to_hex_rgb(), "#123456");
296    }
297
298    #[test]
299    fn color_debug_format() {
300        let c = Color::from_rgb(0x11, 0x22, 0x33);
301        assert_eq!(format!("{c:?}"), "Color(#112233)");
302    }
303
304    #[test]
305    fn color_copy_and_hash() {
306        use std::collections::HashSet;
307        let c = Color::RED;
308        let c2 = c; // Copy
309        assert_eq!(c, c2);
310
311        let mut set = HashSet::new();
312        set.insert(Color::RED);
313        set.insert(Color::GREEN);
314        set.insert(Color::RED); // duplicate
315        assert_eq!(set.len(), 2);
316    }
317
318    #[test]
319    fn color_serde_roundtrip() {
320        let c = Color::from_rgb(0xAA, 0xBB, 0xCC);
321        let json = serde_json::to_string(&c).unwrap();
322        let back: Color = serde_json::from_str(&json).unwrap();
323        assert_eq!(back, c);
324    }
325
326    #[test]
327    fn color_individual_components_isolated() {
328        // Ensure bit manipulation doesn't bleed between components
329        let c = Color::from_rgb(0x01, 0x00, 0x00);
330        assert_eq!(c.red(), 1);
331        assert_eq!(c.green(), 0);
332        assert_eq!(c.blue(), 0);
333
334        let c = Color::from_rgb(0x00, 0x01, 0x00);
335        assert_eq!(c.red(), 0);
336        assert_eq!(c.green(), 1);
337        assert_eq!(c.blue(), 0);
338
339        let c = Color::from_rgb(0x00, 0x00, 0x01);
340        assert_eq!(c.red(), 0);
341        assert_eq!(c.green(), 0);
342        assert_eq!(c.blue(), 1);
343    }
344
345    // ===================================================================
346    // proptest
347    // ===================================================================
348
349    use proptest::prelude::*;
350
351    proptest! {
352        #[test]
353        fn prop_color_rgb_roundtrip(r in 0u8..=255, g in 0u8..=255, b in 0u8..=255) {
354            let c = Color::from_rgb(r, g, b);
355            prop_assert_eq!(c.to_rgb(), (r, g, b));
356        }
357
358        #[test]
359        fn prop_color_raw_preserves_bits(raw in 0u32..=0x00FFFFFF) {
360            let c = Color::from_raw(raw);
361            prop_assert_eq!(c.to_raw(), raw);
362            // Components should reconstruct the raw value
363            let r = c.red() as u32;
364            let g = (c.green() as u32) << 8;
365            let b = (c.blue() as u32) << 16;
366            prop_assert_eq!(r | g | b, raw);
367        }
368    }
369}