Skip to main content

skrifa/
bitmap.rs

1//! Bitmap strikes and glyphs.
2
3use alloc::vec::Vec;
4
5use super::{instance::Size, metrics::GlyphMetrics, MetadataProvider};
6use crate::prelude::LocationRef;
7use raw::{
8    tables::{bitmap, cbdt, cblc, ebdt, eblc, sbix},
9    types::{GlyphId, Tag},
10    FontData, FontRef, TableProvider,
11};
12
13/// Set of strikes, each containing embedded bitmaps of a single size.
14#[derive(Clone)]
15pub struct BitmapStrikes<'a>(StrikesKind<'a>);
16
17impl<'a> BitmapStrikes<'a> {
18    /// Creates a new `BitmapStrikes` for the given font.
19    ///
20    /// This will prefer `sbix`, `CBDT`, and `CBLC` formats in that order.
21    ///
22    /// To select a specific format, use [`with_format`](Self::with_format).
23    pub fn new(font: &FontRef<'a>) -> Self {
24        for format in [BitmapFormat::Sbix, BitmapFormat::Cbdt, BitmapFormat::Ebdt] {
25            if let Some(strikes) = Self::with_format(font, format) {
26                return strikes;
27            }
28        }
29        Self(StrikesKind::None)
30    }
31
32    /// Creates a new `BitmapStrikes` for the given font and format.
33    ///
34    /// Returns `None` if the requested format is not available.
35    pub fn with_format(font: &FontRef<'a>, format: BitmapFormat) -> Option<Self> {
36        let kind = match format {
37            BitmapFormat::Sbix => StrikesKind::Sbix(
38                font.sbix().ok()?,
39                font.glyph_metrics(Size::unscaled(), LocationRef::default()),
40            ),
41            BitmapFormat::Cbdt => {
42                StrikesKind::Cbdt(CbdtTables::new(font.cblc().ok()?, font.cbdt().ok()?))
43            }
44            BitmapFormat::Ebdt => {
45                StrikesKind::Ebdt(EbdtTables::new(font.eblc().ok()?, font.ebdt().ok()?))
46            }
47        };
48        Some(Self(kind))
49    }
50
51    /// Returns the format representing the underlying table for this set of
52    /// strikes.
53    pub fn format(&self) -> Option<BitmapFormat> {
54        match &self.0 {
55            StrikesKind::None => None,
56            StrikesKind::Sbix(..) => Some(BitmapFormat::Sbix),
57            StrikesKind::Cbdt(..) => Some(BitmapFormat::Cbdt),
58            StrikesKind::Ebdt(..) => Some(BitmapFormat::Ebdt),
59        }
60    }
61
62    /// Returns the number of available strikes.
63    pub fn len(&self) -> usize {
64        match &self.0 {
65            StrikesKind::None => 0,
66            StrikesKind::Sbix(sbix, _) => sbix.strikes().len(),
67            StrikesKind::Cbdt(cbdt) => cbdt.location.bitmap_sizes().len(),
68            StrikesKind::Ebdt(ebdt) => ebdt.location.bitmap_sizes().len(),
69        }
70    }
71
72    /// Returns true if there are no available strikes.
73    pub fn is_empty(&self) -> bool {
74        self.len() == 0
75    }
76
77    /// Returns the strike at the given index.
78    pub fn get(&self, index: usize) -> Option<BitmapStrike<'a>> {
79        let kind = match &self.0 {
80            StrikesKind::None => return None,
81            StrikesKind::Sbix(sbix, metrics) => {
82                StrikeKind::Sbix(sbix.strikes().get(index).ok()?, metrics.clone())
83            }
84            StrikesKind::Cbdt(tables) => StrikeKind::Cbdt(
85                tables.location.bitmap_sizes().get(index).copied()?,
86                tables.clone(),
87            ),
88            StrikesKind::Ebdt(tables) => StrikeKind::Ebdt(
89                tables.location.bitmap_sizes().get(index).copied()?,
90                tables.clone(),
91            ),
92        };
93        Some(BitmapStrike(kind))
94    }
95
96    /// Returns the best matching glyph for the given size and glyph
97    /// identifier.
98    ///
99    /// In this case, "best" means a glyph of the exact size, nearest larger
100    /// size, or nearest smaller size, in that order.
101    pub fn glyph_for_size(&self, size: Size, glyph_id: GlyphId) -> Option<BitmapGlyph<'a>> {
102        // Return the largest size for an unscaled request
103        let size = size.ppem().unwrap_or(f32::MAX);
104        self.iter()
105            .fold(None, |best: Option<BitmapGlyph<'a>>, entry| {
106                let entry_size = entry.ppem();
107                if let Some(best) = best {
108                    let best_size = best.ppem_y;
109                    if (entry_size >= size && entry_size < best_size)
110                        || (best_size < size && entry_size > best_size)
111                    {
112                        entry.get(glyph_id).or(Some(best))
113                    } else {
114                        Some(best)
115                    }
116                } else {
117                    entry.get(glyph_id)
118                }
119            })
120    }
121
122    /// Returns an iterator over all available strikes.
123    pub fn iter(&self) -> impl Iterator<Item = BitmapStrike<'a>> + 'a + Clone {
124        let this = self.clone();
125        (0..this.len()).filter_map(move |ix| this.get(ix))
126    }
127}
128
129#[derive(Clone)]
130enum StrikesKind<'a> {
131    None,
132    Sbix(sbix::Sbix<'a>, GlyphMetrics<'a>),
133    Cbdt(CbdtTables<'a>),
134    Ebdt(EbdtTables<'a>),
135}
136
137/// Set of embedded bitmap glyphs of a specific size.
138#[derive(Clone)]
139pub struct BitmapStrike<'a>(StrikeKind<'a>);
140
141impl<'a> BitmapStrike<'a> {
142    /// Returns the pixels-per-em (size) of this strike.
143    pub fn ppem(&self) -> f32 {
144        match &self.0 {
145            StrikeKind::Sbix(sbix, _) => sbix.ppem() as f32,
146            // Original implementation also considers `ppem_y` here:
147            // https://github.com/google/skia/blob/02cd0561f4f756bf4f7b16641d8fc4c61577c765/src/ports/fontations/src/bitmap.rs#L48
148            StrikeKind::Cbdt(size, _) => size.ppem_y() as f32,
149            StrikeKind::Ebdt(size, _) => size.ppem_y() as f32,
150        }
151    }
152
153    /// Returns a bitmap glyph for the given identifier, if available.
154    pub fn get(&self, glyph_id: GlyphId) -> Option<BitmapGlyph<'a>> {
155        match &self.0 {
156            StrikeKind::Sbix(sbix, metrics) => {
157                let glyph = sbix.glyph_data(glyph_id).ok()??;
158                if glyph.graphic_type() != Tag::new(b"png ") {
159                    return None;
160                }
161
162                // Note that this calculation does not entirely correspond to the description in
163                // the specification, but it's implemented this way in Skia (https://github.com/google/skia/blob/02cd0561f4f756bf4f7b16641d8fc4c61577c765/src/ports/fontations/src/bitmap.rs#L161-L178),
164                // the implementation of which has been tested against behavior in CoreText.
165                let (lsb, y_min) = if let Some(y_min) = metrics.glyf_y_min(glyph_id) {
166                    (
167                        metrics.left_side_bearing(glyph_id).unwrap_or_default(),
168                        y_min as f32,
169                    )
170                } else {
171                    (0.0, 0.0)
172                };
173                let ppem = sbix.ppem() as f32;
174                let png_data = glyph.data();
175                // PNG format:
176                // 8 byte header, IHDR chunk (4 byte length, 4 byte chunk type), width, height
177                let reader = FontData::new(png_data);
178                let width = reader.read_at::<u32>(16).ok()?;
179                let height = reader.read_at::<u32>(20).ok()?;
180                Some(BitmapGlyph {
181                    data: BitmapData::Png(glyph.data()),
182                    bearing_x: lsb,
183                    bearing_y: y_min,
184                    inner_bearing_x: glyph.origin_offset_x() as f32,
185                    inner_bearing_y: glyph.origin_offset_y() as f32,
186                    ppem_x: ppem,
187                    ppem_y: ppem,
188                    width,
189                    height,
190                    advance: None,
191                    placement_origin: Origin::BottomLeft,
192                })
193            }
194            StrikeKind::Cbdt(size, tables) => {
195                let location = size
196                    .location(tables.location.offset_data(), glyph_id)
197                    .ok()?;
198                let data = tables.data.data(&location).ok()?;
199                BitmapGlyph::from_bdt(size, &data)
200            }
201            StrikeKind::Ebdt(size, tables) => {
202                let location = size
203                    .location(tables.location.offset_data(), glyph_id)
204                    .ok()?;
205                let data = tables.data.data(&location).ok()?;
206                BitmapGlyph::from_bdt(size, &data)
207            }
208        }
209    }
210}
211
212#[derive(Clone)]
213enum StrikeKind<'a> {
214    Sbix(sbix::Strike<'a>, GlyphMetrics<'a>),
215    Cbdt(bitmap::BitmapSize, CbdtTables<'a>),
216    Ebdt(bitmap::BitmapSize, EbdtTables<'a>),
217}
218
219#[derive(Clone)]
220struct BdtTables<L, D> {
221    location: L,
222    data: D,
223}
224
225impl<L, D> BdtTables<L, D> {
226    fn new(location: L, data: D) -> Self {
227        Self { location, data }
228    }
229}
230
231type CbdtTables<'a> = BdtTables<cblc::Cblc<'a>, cbdt::Cbdt<'a>>;
232type EbdtTables<'a> = BdtTables<eblc::Eblc<'a>, ebdt::Ebdt<'a>>;
233
234/// An embedded bitmap glyph.
235#[derive(Clone)]
236pub struct BitmapGlyph<'a> {
237    /// The underlying data of the bitmap glyph.
238    pub data: BitmapData<'a>,
239    /// Outer glyph bearings in the x direction, given in font units.
240    pub bearing_x: f32,
241    /// Outer glyph bearings in the y direction, given in font units.
242    pub bearing_y: f32,
243    /// Inner glyph bearings in the x direction, given in pixels. This value should be scaled
244    /// by `ppem_*` and be applied as an offset when placing the image within the bounds rectangle.
245    pub inner_bearing_x: f32,
246    /// Inner glyph bearings in the y direction, given in pixels. This value should be scaled
247    /// by `ppem_*` and be applied as an offset when placing the image within the bounds rectangle.
248    pub inner_bearing_y: f32,
249    /// The assumed pixels-per-em in the x direction.
250    pub ppem_x: f32,
251    /// The assumed pixels-per-em in the y direction.
252    pub ppem_y: f32,
253    /// The horizontal advance width of the bitmap glyph in pixels, if given.
254    pub advance: Option<f32>,
255    /// The number of columns in the bitmap.
256    pub width: u32,
257    /// The number of rows in the bitmap.
258    pub height: u32,
259    /// The placement origin of the bitmap.
260    pub placement_origin: Origin,
261}
262
263impl<'a> BitmapGlyph<'a> {
264    fn from_bdt(
265        bitmap_size: &bitmap::BitmapSize,
266        bitmap_data: &bitmap::BitmapData<'a>,
267    ) -> Option<Self> {
268        let metrics = BdtMetrics::new(bitmap_data);
269        let (ppem_x, ppem_y) = (bitmap_size.ppem_x() as f32, bitmap_size.ppem_y() as f32);
270        let bpp = bitmap_size.bit_depth();
271        let data = match bpp {
272            32 => {
273                match &bitmap_data.content {
274                    bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::Png, bytes) => {
275                        BitmapData::Png(bytes)
276                    }
277                    // 32-bit formats are always byte aligned
278                    bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::ByteAligned, bytes) => {
279                        BitmapData::Bgra(bytes)
280                    }
281                    _ => return None,
282                }
283            }
284            1 | 2 | 4 | 8 => {
285                let (data, is_packed) = match &bitmap_data.content {
286                    bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::ByteAligned, bytes) => {
287                        (bytes, false)
288                    }
289                    bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::BitAligned, bytes) => {
290                        (bytes, true)
291                    }
292                    _ => return None,
293                };
294                BitmapData::Mask(MaskData {
295                    bpp,
296                    is_packed,
297                    data,
298                })
299            }
300            // All other bit depth values are invalid
301            _ => return None,
302        };
303        Some(Self {
304            data,
305            bearing_x: 0.0,
306            bearing_y: 0.0,
307            inner_bearing_x: metrics.inner_bearing_x,
308            inner_bearing_y: metrics.inner_bearing_y,
309            ppem_x,
310            ppem_y,
311            width: metrics.width,
312            height: metrics.height,
313            advance: Some(metrics.advance),
314            placement_origin: Origin::TopLeft,
315        })
316    }
317}
318
319struct BdtMetrics {
320    inner_bearing_x: f32,
321    inner_bearing_y: f32,
322    advance: f32,
323    width: u32,
324    height: u32,
325}
326
327impl BdtMetrics {
328    fn new(data: &bitmap::BitmapData) -> Self {
329        match data.metrics {
330            bitmap::BitmapMetrics::Small(metrics) => Self {
331                inner_bearing_x: metrics.bearing_x() as f32,
332                inner_bearing_y: metrics.bearing_y() as f32,
333                advance: metrics.advance() as f32,
334                width: metrics.width() as u32,
335                height: metrics.height() as u32,
336            },
337            bitmap::BitmapMetrics::Big(metrics) => Self {
338                inner_bearing_x: metrics.hori_bearing_x() as f32,
339                inner_bearing_y: metrics.hori_bearing_y() as f32,
340                advance: metrics.hori_advance() as f32,
341                width: metrics.width() as u32,
342                height: metrics.height() as u32,
343            },
344        }
345    }
346}
347
348///The origin point for drawing a bitmap glyph.
349#[derive(Copy, Clone, PartialEq, Eq, Debug)]
350pub enum Origin {
351    /// The origin is in the top-left.
352    TopLeft,
353    /// The origin is in the bottom-left.
354    BottomLeft,
355}
356
357/// Data content of a bitmap.
358#[derive(Clone)]
359pub enum BitmapData<'a> {
360    /// Uncompressed 32-bit color bitmap data, pre-multiplied in BGRA order
361    /// and encoded in the sRGB color space.
362    Bgra(&'a [u8]),
363    /// Compressed PNG bitmap data.
364    Png(&'a [u8]),
365    /// Data representing a single channel alpha mask.
366    Mask(MaskData<'a>),
367}
368
369/// A single channel alpha mask.
370#[derive(Clone)]
371pub struct MaskData<'a> {
372    /// Number of bits-per-pixel. Always 1, 2, 4 or 8.
373    pub bpp: u8,
374    /// True if each row of the data is bit-aligned. Otherwise, each row
375    /// is padded to the next byte.
376    pub is_packed: bool,
377    /// Raw bitmap data.
378    pub data: &'a [u8],
379}
380
381/// Error type returned by [`MaskData::decode`] and [`MaskData::decode_to_slice`].
382#[derive(Copy, Clone, PartialEq, Eq, Debug)]
383pub enum MaskDataDecodeError {
384    /// The width and height product overflows `usize`.
385    SizeOverflow,
386    /// The data buffer is too small for the given dimensions and bit depth.
387    InvalidDimensions,
388}
389
390impl MaskData<'_> {
391    /// Decodes the raw packed bitmap data into 8-bit-per-pixel values,
392    /// writing the result into the provided buffer.
393    ///
394    /// The buffer must be at least `width * height` bytes long. Each pixel
395    /// value is scaled to the 0–255 range.
396    pub fn decode_to_slice(
397        &self,
398        width: u32,
399        height: u32,
400        dst: &mut [u8],
401    ) -> Result<(), MaskDataDecodeError> {
402        let w = width as usize;
403        let h = height as usize;
404        let total_pixels = w.checked_mul(h).ok_or(MaskDataDecodeError::SizeOverflow)?;
405        if total_pixels == 0 {
406            return Ok(());
407        }
408        let bits = self.bpp as usize;
409        if dst.len() < total_pixels {
410            return Err(MaskDataDecodeError::InvalidDimensions);
411        }
412        let dst = &mut dst[..total_pixels];
413        if !self.is_packed {
414            // Byte-aligned: each row is padded to a byte boundary.
415            let row_bytes = (w * bits).div_ceil(8);
416            let expected_data_len = row_bytes
417                .checked_mul(h)
418                .ok_or(MaskDataDecodeError::SizeOverflow)?;
419            if self.data.len() < expected_data_len {
420                return Err(MaskDataDecodeError::InvalidDimensions);
421            }
422            let mut dst_idx = 0;
423            match self.bpp {
424                1 => {
425                    for row in self.data.chunks(row_bytes) {
426                        for x in 0..w {
427                            dst[dst_idx] = ((row[x >> 3] >> (!x & 7)) & 1) * 255;
428                            dst_idx += 1;
429                        }
430                    }
431                }
432                2 => {
433                    for row in self.data.chunks(row_bytes) {
434                        for x in 0..w {
435                            dst[dst_idx] = ((row[x >> 2] >> (!(x * 2) & 6)) & 3) * 85;
436                            dst_idx += 1;
437                        }
438                    }
439                }
440                4 => {
441                    for row in self.data.chunks(row_bytes) {
442                        for x in 0..w {
443                            dst[dst_idx] = ((row[x >> 1] >> (!(x * 4) & 4)) & 15) * 17;
444                            dst_idx += 1;
445                        }
446                    }
447                }
448                8 => {
449                    for row in self.data.chunks(row_bytes) {
450                        dst[dst_idx..dst_idx + w].copy_from_slice(&row[..w]);
451                        dst_idx += w;
452                    }
453                }
454                _ => return Err(MaskDataDecodeError::InvalidDimensions),
455            }
456        } else {
457            // Bit-aligned: pixels are tightly packed with no row padding.
458            let total_bits = total_pixels
459                .checked_mul(bits)
460                .ok_or(MaskDataDecodeError::SizeOverflow)?;
461            let expected_data_len = total_bits.div_ceil(8);
462            if self.data.len() < expected_data_len {
463                return Err(MaskDataDecodeError::InvalidDimensions);
464            }
465            match self.bpp {
466                1 => {
467                    for (x, pixel) in dst.iter_mut().enumerate() {
468                        *pixel = ((self.data[x >> 3] >> (!x & 7)) & 1) * 255;
469                    }
470                }
471                2 => {
472                    for (x, pixel) in dst.iter_mut().enumerate() {
473                        *pixel = ((self.data[x >> 2] >> (!(x * 2) & 6)) & 3) * 85;
474                    }
475                }
476                4 => {
477                    for (x, pixel) in dst.iter_mut().enumerate() {
478                        *pixel = ((self.data[x >> 1] >> (!(x * 4) & 4)) & 15) * 17;
479                    }
480                }
481                8 => {
482                    dst.copy_from_slice(&self.data[..total_pixels]);
483                }
484                _ => return Err(MaskDataDecodeError::InvalidDimensions),
485            }
486        }
487        Ok(())
488    }
489
490    /// Decodes the raw packed bitmap data into 8-bit-per-pixel values.
491    ///
492    /// Returns a `Vec<u8>` of `width * height` bytes, with each pixel
493    /// value scaled to the 0–255 range.
494    pub fn decode(&self, width: u32, height: u32) -> Result<Vec<u8>, MaskDataDecodeError> {
495        let w = width as usize;
496        let h = height as usize;
497        let total_pixels = w.checked_mul(h).ok_or(MaskDataDecodeError::SizeOverflow)?;
498        let mut dst = vec![0u8; total_pixels];
499        self.decode_to_slice(width, height, &mut dst)?;
500        Ok(dst)
501    }
502}
503
504/// The format (or table) containing the data backing a set of bitmap strikes.
505#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
506pub enum BitmapFormat {
507    Sbix,
508    Cbdt,
509    Ebdt,
510}
511
512#[cfg(test)]
513mod tests {
514    use crate::bitmap::{BitmapData, MaskData, MaskDataDecodeError, StrikesKind};
515    use crate::prelude::Size;
516    use crate::{GlyphId, MetadataProvider};
517    use raw::FontRef;
518
519    #[test]
520    fn cbdt_metadata() {
521        let font = FontRef::new(font_test_data::CBDT).unwrap();
522        let strikes = font.bitmap_strikes();
523
524        assert!(matches!(strikes.0, StrikesKind::Cbdt(_)));
525        assert!(matches!(strikes.len(), 3));
526
527        // Note that this is only `ppem_y`.
528        assert!(matches!(strikes.get(0).unwrap().ppem(), 16.0));
529        assert!(matches!(strikes.get(1).unwrap().ppem(), 64.0));
530        assert!(matches!(strikes.get(2).unwrap().ppem(), 128.0));
531    }
532
533    #[test]
534    fn cbdt_glyph_metrics() {
535        let font = FontRef::new(font_test_data::CBDT).unwrap();
536        let strike_0 = font.bitmap_strikes().get(0).unwrap();
537
538        let zero = strike_0.get(GlyphId::new(0)).unwrap();
539        assert_eq!(zero.width, 11);
540        assert_eq!(zero.height, 13);
541        assert_eq!(zero.bearing_x, 0.0);
542        assert_eq!(zero.bearing_y, 0.0);
543        assert_eq!(zero.inner_bearing_x, 1.0);
544        assert_eq!(zero.inner_bearing_y, 13.0);
545        assert_eq!(zero.advance, Some(12.0));
546
547        let strike_1 = font.bitmap_strikes().get(1).unwrap();
548
549        let zero = strike_1.get(GlyphId::new(2)).unwrap();
550        assert_eq!(zero.width, 39);
551        assert_eq!(zero.height, 52);
552        assert_eq!(zero.bearing_x, 0.0);
553        assert_eq!(zero.bearing_y, 0.0);
554        assert_eq!(zero.inner_bearing_x, 6.0);
555        assert_eq!(zero.inner_bearing_y, 52.0);
556        assert_eq!(zero.advance, Some(51.0));
557    }
558
559    #[test]
560    fn cbdt_glyph_selection() {
561        let font = FontRef::new(font_test_data::CBDT).unwrap();
562        let strikes = font.bitmap_strikes();
563
564        let g1 = strikes
565            .glyph_for_size(Size::new(12.0), GlyphId::new(2))
566            .unwrap();
567        assert_eq!(g1.ppem_x, 16.0);
568
569        let g2 = strikes
570            .glyph_for_size(Size::new(17.0), GlyphId::new(2))
571            .unwrap();
572        assert_eq!(g2.ppem_x, 64.0);
573
574        let g3 = strikes
575            .glyph_for_size(Size::new(60.0), GlyphId::new(2))
576            .unwrap();
577        assert_eq!(g3.ppem_x, 64.0);
578
579        let g4 = strikes
580            .glyph_for_size(Size::unscaled(), GlyphId::new(2))
581            .unwrap();
582        assert_eq!(g4.ppem_x, 128.0);
583    }
584
585    #[test]
586    fn sbix_metadata() {
587        let font = FontRef::new(font_test_data::NOTO_HANDWRITING_SBIX).unwrap();
588        let strikes = font.bitmap_strikes();
589
590        assert!(matches!(strikes.0, StrikesKind::Sbix(_, _)));
591        assert!(matches!(strikes.len(), 1));
592
593        assert!(matches!(strikes.get(0).unwrap().ppem(), 109.0));
594    }
595
596    #[test]
597    fn sbix_glyph_metrics() {
598        let font = FontRef::new(font_test_data::NOTO_HANDWRITING_SBIX).unwrap();
599        let strike_0 = font.bitmap_strikes().get(0).unwrap();
600
601        let g0 = strike_0.get(GlyphId::new(7)).unwrap();
602        // `bearing_x` is always the lsb, which is 0 for this glyph.
603        assert_eq!(g0.bearing_x, 0.0);
604        // The glyph doesn't have an associated outline, so `bbox.min_y` is 0, and thus bearing_y
605        // should also be 0.
606
607        assert_eq!(g0.bearing_y, 0.0);
608        // Origin offsets are 4.0 and -27.0 respectively.
609        assert_eq!(g0.inner_bearing_x, 4.0);
610        assert_eq!(g0.inner_bearing_y, -27.0);
611        assert!(matches!(g0.data, BitmapData::Png(_)))
612    }
613
614    #[test]
615    fn decode_1bpp_non_packed() {
616        // 4×2 image, 1 bpp, byte-aligned (each row padded to 1 byte).
617        // Row 0: pixels [1,0,1,0] → 0b1010_0000 = 0xA0
618        // Row 1: pixels [0,1,0,1] → 0b0101_0000 = 0x50
619        let mask = MaskData {
620            bpp: 1,
621            is_packed: false,
622            data: &[0xA0, 0x50],
623        };
624        let decoded = mask.decode(4, 2).unwrap();
625        assert_eq!(decoded, [255, 0, 255, 0, 0, 255, 0, 255],);
626    }
627
628    #[test]
629    fn decode_2bpp_non_packed() {
630        // 4×1 image, 2 bpp, byte-aligned.
631        // Pixels [3, 2, 1, 0] → 0b11_10_01_00 = 0xE4
632        // Scaled by 85: [255, 170, 85, 0]
633        let mask = MaskData {
634            bpp: 2,
635            is_packed: false,
636            data: &[0xE4],
637        };
638        let decoded = mask.decode(4, 1).unwrap();
639        assert_eq!(decoded, [255, 170, 85, 0]);
640    }
641
642    #[test]
643    fn decode_4bpp_non_packed() {
644        // 3×2 image, 4 bpp, byte-aligned.
645        // row_bytes = ceil(3*4 / 8) = 2
646        // Row 0: pixels [15, 8, 4]
647        //   byte 0: (15 << 4) | 8 = 0xF8
648        //   byte 1: (4  << 4) | 0 = 0x40  (low nibble is padding)
649        // Row 1: pixels [0, 5, 10]
650        //   byte 0: (0  << 4) | 5 = 0x05
651        //   byte 1: (10 << 4) | 0 = 0xA0
652        // Scaled by 17: [255, 136, 68, 0, 85, 170]
653        let mask = MaskData {
654            bpp: 4,
655            is_packed: false,
656            data: &[0xF8, 0x40, 0x05, 0xA0],
657        };
658        let decoded = mask.decode(3, 2).unwrap();
659        assert_eq!(decoded, [255, 136, 68, 0, 85, 170]);
660    }
661
662    #[test]
663    fn decode_8bpp_non_packed() {
664        // 3×2 image, 8 bpp, byte-aligned (trivial copy).
665        let mask = MaskData {
666            bpp: 8,
667            is_packed: false,
668            data: &[10, 20, 30, 40, 50, 60],
669        };
670        let decoded = mask.decode(3, 2).unwrap();
671        assert_eq!(decoded, [10, 20, 30, 40, 50, 60]);
672    }
673
674    #[test]
675    fn decode_1bpp_packed() {
676        // 3×3 image, 1 bpp, bit-aligned (packed, no row padding).
677        // 9 pixels packed into 2 bytes, MSB first.
678        // Pixels: [1,0,1, 0,1,0, 1,1,0]
679        // Bits:    1 0 1 0 1 0 1 1 | 0 x x x x x x x
680        // Byte 0: 0b10101011 = 0xAB
681        // Byte 1: 0b00000000 = 0x00 (only MSB used)
682        let mask = MaskData {
683            bpp: 1,
684            is_packed: true,
685            data: &[0xAB, 0x00],
686        };
687        let decoded = mask.decode(3, 3).unwrap();
688        assert_eq!(decoded, [255, 0, 255, 0, 255, 0, 255, 255, 0],);
689    }
690
691    #[test]
692    fn decode_2bpp_packed() {
693        // 5×2 image, 2 bpp, bit-aligned (packed, no row padding).
694        // 10 pixels × 2 bits = 20 bits = 3 bytes (last 4 bits unused).
695        // Pixels: [3, 2, 1, 0, 3,  0, 1, 2, 3, 0]
696        // Byte 0: 0b11_10_01_00 = 0xE4  (pixels 0–3)
697        // Byte 1: 0b11_00_01_10 = 0xC6  (pixels 4–7)
698        // Byte 2: 0b11_00_0000 = 0xC0   (pixels 8–9, rest padding)
699        // Scaled by 85: [255, 170, 85, 0, 255, 0, 85, 170, 255, 0]
700        let mask = MaskData {
701            bpp: 2,
702            is_packed: true,
703            data: &[0xE4, 0xC6, 0xC0],
704        };
705        let decoded = mask.decode(5, 2).unwrap();
706        assert_eq!(decoded, [255, 170, 85, 0, 255, 0, 85, 170, 255, 0]);
707    }
708
709    #[test]
710    fn decode_4bpp_packed() {
711        // 3×2 image, 4 bpp, bit-aligned (packed, no row padding).
712        // 6 pixels × 4 bits = 24 bits = 3 bytes exactly.
713        // Pixels: [15, 0, 8, 4, 10, 5]
714        // Byte 0: (15 << 4) | 0  = 0xF0
715        // Byte 1: (8  << 4) | 4  = 0x84
716        // Byte 2: (10 << 4) | 5  = 0xA5
717        // Scaled by 17: [255, 0, 136, 68, 170, 85]
718        let mask = MaskData {
719            bpp: 4,
720            is_packed: true,
721            data: &[0xF0, 0x84, 0xA5],
722        };
723        let decoded = mask.decode(3, 2).unwrap();
724        assert_eq!(decoded, [255, 0, 136, 68, 170, 85]);
725    }
726
727    #[test]
728    fn decode_8bpp_packed() {
729        // 3×2 image, 8 bpp, bit-aligned (packed, trivial copy).
730        // Each pixel is one byte, so packed and non-packed are equivalent.
731        let mask = MaskData {
732            bpp: 8,
733            is_packed: true,
734            data: &[100, 200, 50, 0, 128, 255],
735        };
736        let decoded = mask.decode(3, 2).unwrap();
737        assert_eq!(decoded, [100, 200, 50, 0, 128, 255]);
738    }
739
740    #[test]
741    fn decode_error_cases() {
742        // Zero dimensions return Ok with empty output.
743        let mask = MaskData {
744            bpp: 8,
745            is_packed: false,
746            data: &[],
747        };
748        assert!(mask.decode(0, 0).unwrap().is_empty());
749        assert!(mask.decode(0, 5).unwrap().is_empty());
750        assert!(mask.decode(5, 0).unwrap().is_empty());
751
752        // Data too short for non-packed.
753        let mask = MaskData {
754            bpp: 8,
755            is_packed: false,
756            data: &[1, 2, 3],
757        };
758        assert_eq!(
759            mask.decode(4, 2),
760            Err(MaskDataDecodeError::InvalidDimensions)
761        );
762
763        // Data too short for packed (9 pixels at 1 bpp needs 2 bytes).
764        let mask = MaskData {
765            bpp: 1,
766            is_packed: true,
767            data: &[0xFF],
768        };
769        assert_eq!(
770            mask.decode(3, 3),
771            Err(MaskDataDecodeError::InvalidDimensions)
772        );
773    }
774
775    #[test]
776    fn decode_to_slice_basic_and_errors() {
777        let mask = MaskData {
778            bpp: 8,
779            is_packed: false,
780            data: &[10, 20, 30, 40, 50, 60],
781        };
782
783        // Successful decode into a provided buffer.
784        let mut buf = [0u8; 6];
785        mask.decode_to_slice(3, 2, &mut buf).unwrap();
786        assert_eq!(buf, [10, 20, 30, 40, 50, 60]);
787
788        // Output buffer too small.
789        let mut small_buf = [0u8; 4];
790        assert_eq!(
791            mask.decode_to_slice(3, 2, &mut small_buf),
792            Err(MaskDataDecodeError::InvalidDimensions)
793        );
794
795        // Zero dimensions succeed with empty buffer.
796        let empty = MaskData {
797            bpp: 8,
798            is_packed: false,
799            data: &[],
800        };
801        let mut empty_buf = [0u8; 0];
802        empty.decode_to_slice(0, 0, &mut empty_buf).unwrap();
803    }
804}