Skip to main content

rasterrocket_render/
glyph.rs

1//! Glyph bitmap blitting — replaces `Splash::fillGlyph2`.
2//!
3//! A glyph bitmap is either:
4//! - **AA** (`aa = true`): one byte per pixel, containing a coverage value 0–255.
5//! - **Mono** (`aa = false`): 1-bit-per-pixel, MSB-first packed, `ceil(w/8)` bytes per row.
6//!
7//! The blit clips to the destination bitmap bounds and optionally to the `Clip` region.
8//!
9//! # C++ equivalent
10//! `Splash::fillGlyph2`.
11
12use crate::bitmap::Bitmap;
13use crate::clip::{Clip, ClipResult};
14use crate::pipe::{self, PipeSrc, PipeState};
15use crate::simd;
16use color::Pixel;
17
18/// A rasterized glyph bitmap as produced by a font renderer.
19///
20/// The origin `(x, y)` is the pen-position offset in device pixels:
21/// the top-left of the bitmap maps to `(pen_x - x, pen_y - y)`.
22pub struct GlyphBitmap<'a> {
23    /// Raw bitmap data. For AA glyphs: `w` bytes per row. For mono: `ceil(w/8)` bytes per row.
24    pub data: &'a [u8],
25    /// Horizontal offset from the pen position to the left edge of the bitmap.
26    pub x: i32,
27    /// Vertical offset from the pen position to the top edge of the bitmap.
28    pub y: i32,
29    /// Width in pixels.
30    pub w: i32,
31    /// Height in pixels.
32    pub h: i32,
33    /// `true` for anti-aliased (one u8 coverage byte per pixel);
34    /// `false` for 1-bit MSB-first packed mono.
35    pub aa: bool,
36}
37
38impl GlyphBitmap<'_> {
39    /// Number of bytes per row in `data`.
40    #[must_use]
41    pub fn row_bytes(&self) -> usize {
42        let w = self.w.max(0);
43        if self.aa {
44            #[expect(clippy::cast_sign_loss, reason = "w = self.w.max(0) is non-negative")]
45            {
46                w as usize
47            }
48        } else {
49            // Use saturating_add so a pathological w near i32::MAX doesn't wrap.
50            // In practice FreeType glyphs are always < 2^16 px wide.
51            #[expect(clippy::cast_sign_loss, reason = "w = self.w.max(0) is non-negative")]
52            {
53                (w.saturating_add(7) / 8) as usize
54            }
55        }
56    }
57}
58
59/// Blit a glyph at pen position `(pen_x, pen_y)` (device pixel coordinates).
60///
61/// `clip_all_inside` — if `true`, skip per-pixel clip tests (the caller has
62/// already determined the entire glyph bbox is inside the clip region).
63/// When `false`, the `clip` is tested per pixel.
64///
65/// The `pipe` and `src` describe the fill colour and compositing parameters.
66///
67/// # C++ equivalent
68///
69/// Matches `Splash::fillGlyph2` with `noClip = clip_all_inside`.
70#[expect(
71    clippy::too_many_arguments,
72    reason = "mirrors Splash::fillGlyph2 API; all params necessary"
73)]
74pub fn blit_glyph<P: Pixel>(
75    bitmap: &mut Bitmap<P>,
76    clip: &Clip,
77    clip_all_inside: bool,
78    pipe: &PipeState<'_>,
79    src: &PipeSrc<'_>,
80    pen_x: i32,
81    pen_y: i32,
82    glyph: &GlyphBitmap<'_>,
83) {
84    // Compute the top-left destination pixel.
85    let x_start_raw = pen_x - glyph.x;
86    let y_start_raw = pen_y - glyph.y;
87
88    // Clamp to bitmap bounds.
89    #[expect(
90        clippy::cast_possible_wrap,
91        reason = "bitmap dims ≤ i32::MAX in practice"
92    )]
93    let bmp_w = bitmap.width as i32;
94    #[expect(
95        clippy::cast_possible_wrap,
96        reason = "bitmap dims ≤ i32::MAX in practice"
97    )]
98    let bmp_h = bitmap.height as i32;
99
100    let x_clip = x_start_raw.max(0);
101    let y_clip = y_start_raw.max(0);
102    let x_end = (x_start_raw + glyph.w).min(bmp_w);
103    let y_end = (y_start_raw + glyph.h).min(bmp_h);
104
105    if x_clip >= x_end || y_clip >= y_end {
106        return;
107    }
108
109    // All four differences are non-negative because of the checks above.
110    #[expect(
111        clippy::cast_sign_loss,
112        reason = "x_clip ≥ x_start_raw.max(0) so difference ≥ 0"
113    )]
114    let x_data_skip = (x_clip - x_start_raw) as usize;
115    #[expect(
116        clippy::cast_sign_loss,
117        reason = "y_clip ≥ y_start_raw.max(0) so difference ≥ 0"
118    )]
119    let y_data_skip = (y_clip - y_start_raw) as usize;
120    #[expect(clippy::cast_sign_loss, reason = "x_end > x_clip by guard above")]
121    let xx_limit = (x_end - x_clip) as usize;
122    #[expect(clippy::cast_sign_loss, reason = "y_end > y_clip by guard above")]
123    let yy_limit = (y_end - y_clip) as usize;
124    let row_bytes = glyph.row_bytes();
125
126    if glyph.aa {
127        blit_aa::<P>(
128            bitmap,
129            clip,
130            clip_all_inside,
131            pipe,
132            src,
133            glyph,
134            x_clip,
135            y_clip,
136            x_data_skip,
137            y_data_skip,
138            xx_limit,
139            yy_limit,
140            row_bytes,
141        );
142    } else {
143        blit_mono::<P>(
144            bitmap,
145            clip,
146            clip_all_inside,
147            pipe,
148            src,
149            glyph,
150            x_clip,
151            y_clip,
152            x_data_skip,
153            y_data_skip,
154            xx_limit,
155            yy_limit,
156            row_bytes,
157        );
158    }
159}
160
161/// Blit an AA (per-byte coverage) glyph.
162#[expect(
163    clippy::too_many_arguments,
164    reason = "internal helper; all params necessary for this blit path"
165)]
166fn blit_aa<P: Pixel>(
167    bitmap: &mut Bitmap<P>,
168    clip: &Clip,
169    clip_all_inside: bool,
170    pipe: &PipeState<'_>,
171    src: &PipeSrc<'_>,
172    glyph: &GlyphBitmap<'_>,
173    x_start: i32,
174    y_start: i32,
175    x_data_skip: usize,
176    y_data_skip: usize,
177    xx_limit: usize,
178    yy_limit: usize,
179    row_bytes: usize,
180) {
181    let data = glyph.data;
182    // Verify the glyph data buffer covers the visible region.  A mismatch
183    // indicates a malformed GlyphBitmap (wrong w/h/row_bytes); we clamp via
184    // get() in the inner loop, but the assert catches it in debug builds.
185    debug_assert!(
186        data.len() >= (y_data_skip + yy_limit) * row_bytes,
187        "blit_aa: glyph data too short: len={} < (y_data_skip={y_data_skip} + yy_limit={yy_limit}) * row_bytes={row_bytes}",
188        data.len(),
189    );
190    // Hoisted above the row loop to avoid per-row heap allocation.
191    let mut run_shape: Vec<u8> = Vec::new();
192
193    for yy in 0..yy_limit {
194        #[expect(
195            clippy::cast_possible_truncation,
196            reason = "yy < yy_limit ≤ bitmap.height ≤ i32::MAX"
197        )]
198        #[expect(clippy::cast_possible_wrap, reason = "yy < bitmap.height ≤ i32::MAX")]
199        let y = y_start + yy as i32;
200        let row_off = (y_data_skip + yy) * row_bytes + x_data_skip;
201
202        let mut run_start: Option<i32> = None;
203        run_shape.clear();
204
205        for xx in 0..xx_limit {
206            #[expect(
207                clippy::cast_possible_truncation,
208                reason = "xx < xx_limit ≤ bitmap.width ≤ i32::MAX"
209            )]
210            #[expect(clippy::cast_possible_wrap, reason = "xx < bitmap.width ≤ i32::MAX")]
211            let x = x_start + xx as i32;
212            let data_idx = row_off + xx;
213            let alpha = data.get(data_idx).copied().unwrap_or(0);
214            let inside_clip = clip_all_inside || clip.test(x, y);
215
216            if inside_clip && alpha != 0 {
217                if run_start.is_none() {
218                    run_start = Some(x);
219                    run_shape.clear();
220                }
221                run_shape.push(alpha);
222            } else if let Some(rs) = run_start.take() {
223                emit_aa_run::<P>(bitmap, pipe, src, rs, y, &run_shape);
224            }
225        }
226        if let Some(rs) = run_start.take() {
227            emit_aa_run::<P>(bitmap, pipe, src, rs, y, &run_shape);
228        }
229    }
230}
231
232/// Blit a mono (1-bit packed) glyph.
233#[expect(
234    clippy::too_many_arguments,
235    reason = "internal helper; all params necessary"
236)]
237#[expect(
238    clippy::too_many_lines,
239    reason = "function handles both SIMD and scalar mono-unpack paths; splitting further would obscure the logic"
240)]
241fn blit_mono<P: Pixel>(
242    bitmap: &mut Bitmap<P>,
243    clip: &Clip,
244    clip_all_inside: bool,
245    pipe: &PipeState<'_>,
246    src: &PipeSrc<'_>,
247    glyph: &GlyphBitmap<'_>,
248    x_start: i32,
249    y_start: i32,
250    x_data_skip: usize,
251    y_data_skip: usize,
252    xx_limit: usize,
253    yy_limit: usize,
254    row_bytes: usize,
255) {
256    let x_shift = x_data_skip % 8;
257    let data = glyph.data;
258
259    // Verify the glyph data buffer covers the visible region (debug builds only).
260    debug_assert!(
261        data.len() >= (y_data_skip + yy_limit) * row_bytes,
262        "blit_mono: glyph data too short: len={} < (y_data_skip={y_data_skip} + yy_limit={yy_limit}) * row_bytes={row_bytes}",
263        data.len(),
264    );
265
266    // Scratch buffer for SIMD-expanded bits: one byte per pixel, 0x00 or 0xFF.
267    // Sized to the maximum row width; reused across rows.
268    let mut expanded: Vec<u8> = vec![0u8; xx_limit];
269
270    for yy in 0..yy_limit {
271        #[expect(
272            clippy::cast_possible_truncation,
273            reason = "yy < yy_limit ≤ bitmap.height ≤ i32::MAX"
274        )]
275        #[expect(clippy::cast_possible_wrap, reason = "yy < bitmap.height ≤ i32::MAX")]
276        let y = y_start + yy as i32;
277        let row_off = (y_data_skip + yy) * row_bytes + x_data_skip / 8;
278
279        // Use SIMD unpack when x_shift == 0 (bits are byte-aligned).
280        // When x_shift > 0 the pixels straddle byte boundaries, so we fall
281        // back to the scalar bit-extraction path which handles that case.
282        #[cfg(target_arch = "x86_64")]
283        let use_simd_unpack = x_shift == 0;
284        #[cfg(not(target_arch = "x86_64"))]
285        let use_simd_unpack = false;
286
287        if use_simd_unpack {
288            let packed_row = &data[row_off..];
289            simd::unpack_mono_row(packed_row, xx_limit, &mut expanded);
290
291            let mut run_start: Option<i32> = None;
292            for (xx, &cov) in expanded[..xx_limit].iter().enumerate() {
293                #[expect(
294                    clippy::cast_possible_truncation,
295                    reason = "xx < xx_limit ≤ bitmap.width ≤ i32::MAX"
296                )]
297                #[expect(clippy::cast_possible_wrap, reason = "xx < bitmap.width ≤ i32::MAX")]
298                let x = x_start + xx as i32;
299                let set = cov != 0;
300                let inside_clip = clip_all_inside || clip.test(x, y);
301
302                if set && inside_clip {
303                    if run_start.is_none() {
304                        run_start = Some(x);
305                    }
306                } else if let Some(rs) = run_start.take() {
307                    let rx1 = x - 1;
308                    emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
309                }
310            }
311            if let Some(rs) = run_start.take() {
312                #[expect(
313                    clippy::cast_possible_truncation,
314                    reason = "xx_limit ≤ bitmap.width ≤ i32::MAX"
315                )]
316                #[expect(
317                    clippy::cast_possible_wrap,
318                    reason = "xx_limit < bitmap.width ≤ i32::MAX"
319                )]
320                let rx1 = x_start + xx_limit as i32 - 1;
321                emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
322            }
323            continue;
324        }
325
326        let mut run_start: Option<i32> = None;
327        let mut xx = 0usize;
328
329        while xx < xx_limit {
330            let byte_idx = row_off + xx / 8;
331
332            // When x_shift > 0, straddle two source bytes to align the read window.
333            let alpha0 = if x_shift > 0 && xx + 8 < xx_limit {
334                let lo = data.get(byte_idx).copied().unwrap_or(0);
335                let hi = data.get(byte_idx + 1).copied().unwrap_or(0);
336                #[expect(clippy::cast_possible_truncation, reason = "shift result fits in u8")]
337                {
338                    (u16::from(lo) << x_shift | u16::from(hi) >> (8 - x_shift)) as u8
339                }
340            } else {
341                data.get(byte_idx).copied().unwrap_or(0)
342            };
343
344            let bits_this_byte = (xx_limit - xx).min(8);
345            for bit in 0..bits_this_byte {
346                #[expect(
347                    clippy::cast_possible_truncation,
348                    reason = "xx + bit < xx_limit ≤ bitmap.width ≤ i32::MAX"
349                )]
350                #[expect(
351                    clippy::cast_possible_wrap,
352                    reason = "xx + bit < bitmap.width ≤ i32::MAX"
353                )]
354                let x = x_start + (xx + bit) as i32;
355                let set = (alpha0 >> (7 - bit)) & 1 != 0;
356                let inside_clip = clip_all_inside || clip.test(x, y);
357
358                if set && inside_clip {
359                    if run_start.is_none() {
360                        run_start = Some(x);
361                    }
362                } else if let Some(rs) = run_start.take() {
363                    let rx1 = x - 1;
364                    emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
365                }
366            }
367            xx += bits_this_byte;
368        }
369        if let Some(rs) = run_start.take() {
370            #[expect(
371                clippy::cast_possible_truncation,
372                reason = "xx_limit ≤ bitmap.width ≤ i32::MAX"
373            )]
374            #[expect(
375                clippy::cast_possible_wrap,
376                reason = "xx_limit < bitmap.width ≤ i32::MAX"
377            )]
378            let rx1 = x_start + xx_limit as i32 - 1;
379            emit_solid_run::<P>(bitmap, pipe, src, rs, rx1, y);
380        }
381    }
382}
383
384/// Emit one AA-shaped span via `render_span`.
385fn emit_aa_run<P: Pixel>(
386    bitmap: &mut Bitmap<P>,
387    pipe: &PipeState<'_>,
388    src: &PipeSrc<'_>,
389    x0: i32,
390    y: i32,
391    shape: &[u8],
392) {
393    debug_assert!(!shape.is_empty());
394    // shape.len() ≤ bitmap.width ≤ i32::MAX in practice.
395    #[expect(
396        clippy::cast_possible_truncation,
397        reason = "shape.len() ≤ bitmap.width ≤ i32::MAX"
398    )]
399    #[expect(
400        clippy::cast_possible_wrap,
401        reason = "shape.len() ≤ bitmap.width ≤ i32::MAX"
402    )]
403    let x1 = x0 + shape.len() as i32 - 1;
404    #[expect(clippy::cast_sign_loss, reason = "y ≥ 0 by construction")]
405    let y_u = y as u32;
406    #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0")]
407    let byte_off = x0 as usize * P::BYTES;
408    #[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0")]
409    let byte_end = (x1 as usize + 1) * P::BYTES;
410    #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0, x1 ≥ x0")]
411    let alpha_range = x0 as usize..=x1 as usize;
412
413    let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
414    let dst_pixels = &mut row[byte_off..byte_end];
415    let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
416
417    pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, Some(shape), x0, x1, y);
418}
419
420/// Emit a fully opaque span (mono glyph pixels) via `render_span`.
421fn emit_solid_run<P: Pixel>(
422    bitmap: &mut Bitmap<P>,
423    pipe: &PipeState<'_>,
424    src: &PipeSrc<'_>,
425    x0: i32,
426    x1: i32,
427    y: i32,
428) {
429    debug_assert!(x0 <= x1);
430    #[expect(clippy::cast_sign_loss, reason = "y ≥ 0 by construction")]
431    let y_u = y as u32;
432    #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0")]
433    let byte_off = x0 as usize * P::BYTES;
434    #[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0")]
435    let byte_end = (x1 as usize + 1) * P::BYTES;
436    #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0, x1 ≥ x0")]
437    let alpha_range = x0 as usize..=x1 as usize;
438
439    let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
440    let dst_pixels = &mut row[byte_off..byte_end];
441    let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
442
443    pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, None, x0, x1, y);
444}
445
446/// Clip-test a glyph bbox and blit it.
447///
448/// Convenience wrapper that performs the `testRect` + `fillGlyph2` pattern from
449/// `Splash::fillGlyph`. Returns the `ClipResult` for the caller to record as `opClipRes`.
450pub fn fill_glyph<P: Pixel>(
451    bitmap: &mut Bitmap<P>,
452    clip: &Clip,
453    pipe: &PipeState<'_>,
454    src: &PipeSrc<'_>,
455    pen_x: i32,
456    pen_y: i32,
457    glyph: &GlyphBitmap<'_>,
458) -> ClipResult {
459    let x0 = pen_x - glyph.x;
460    let y0 = pen_y - glyph.y;
461    let x1 = x0 + glyph.w - 1;
462    let y1 = y0 + glyph.h - 1;
463
464    let clip_res = clip.test_rect(x0, y0, x1, y1);
465    if clip_res != ClipResult::AllOutside {
466        blit_glyph::<P>(
467            bitmap,
468            clip,
469            clip_res == ClipResult::AllInside,
470            pipe,
471            src,
472            pen_x,
473            pen_y,
474            glyph,
475        );
476    }
477    clip_res
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::bitmap::Bitmap;
484    use crate::pipe::PipeSrc;
485    use crate::testutil::{make_clip, simple_pipe};
486    use color::Rgb8;
487
488    #[test]
489    fn blit_aa_glyph_paints_pixels() {
490        let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
491        let clip = make_clip(8, 8);
492        let pipe = simple_pipe();
493        let color = [255u8, 0, 0];
494        let src = PipeSrc::Solid(&color);
495
496        // 4×4 AA glyph with full coverage (255) everywhere.
497        let data = vec![255u8; 16];
498        let glyph = GlyphBitmap {
499            data: &data,
500            x: 0,
501            y: 0,
502            w: 4,
503            h: 4,
504            aa: true,
505        };
506
507        blit_glyph::<Rgb8>(&mut bmp, &clip, true, &pipe, &src, 2, 2, &glyph);
508
509        // Rows 2..5, cols 2..5 should be red.
510        for y in 2..6u32 {
511            let row = bmp.row(y);
512            for (x, px) in row.iter().enumerate().skip(2).take(4) {
513                assert_eq!(px.r, 255, "y={y} x={x} R");
514            }
515        }
516        assert_eq!(bmp.row(1)[1].r, 0);
517    }
518
519    #[test]
520    fn blit_aa_glyph_zero_coverage_skips() {
521        let mut bmp: Bitmap<Rgb8> = Bitmap::new(4, 4, 4, false);
522        let clip = make_clip(4, 4);
523        let pipe = simple_pipe();
524        let color = [255u8, 0, 0];
525        let src = PipeSrc::Solid(&color);
526
527        let data = vec![0u8; 4];
528        let glyph = GlyphBitmap {
529            data: &data,
530            x: 0,
531            y: 0,
532            w: 2,
533            h: 2,
534            aa: true,
535        };
536
537        blit_glyph::<Rgb8>(&mut bmp, &clip, true, &pipe, &src, 0, 0, &glyph);
538
539        for y in 0..4u32 {
540            let row = bmp.row(y);
541            for px in row.iter().take(4) {
542                assert_eq!(px.r, 0, "should be unpainted");
543            }
544        }
545    }
546
547    #[test]
548    fn blit_mono_glyph_paints_set_bits() {
549        let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 4, 4, false);
550        let clip = make_clip(8, 4);
551        let pipe = simple_pipe();
552        let color = [0u8, 255, 0];
553        let src = PipeSrc::Solid(&color);
554
555        // 8×2 mono glyph: row 0 all-set (0xFF), row 1 all-clear (0x00).
556        let data = [0xFFu8, 0x00u8];
557        let glyph = GlyphBitmap {
558            data: &data,
559            x: 0,
560            y: 0,
561            w: 8,
562            h: 2,
563            aa: false,
564        };
565
566        blit_glyph::<Rgb8>(&mut bmp, &clip, true, &pipe, &src, 0, 0, &glyph);
567
568        let row0 = bmp.row(0);
569        for (x, px) in row0.iter().enumerate().take(8) {
570            assert_eq!(px.g, 255, "row 0 x={x} should be painted");
571        }
572        let row1 = bmp.row(1);
573        for (x, px) in row1.iter().enumerate().take(8) {
574            assert_eq!(px.g, 0, "row 1 x={x} should be clear");
575        }
576    }
577
578    #[test]
579    fn blit_glyph_clip_excludes_outside() {
580        let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
581        let clip = Clip::new(2.0, 2.0, 5.0, 5.0, false);
582        let pipe = simple_pipe();
583        let color = [255u8, 0, 0];
584        let src = PipeSrc::Solid(&color);
585
586        let data = vec![255u8; 16];
587        let glyph = GlyphBitmap {
588            data: &data,
589            x: 0,
590            y: 0,
591            w: 4,
592            h: 4,
593            aa: true,
594        };
595
596        blit_glyph::<Rgb8>(&mut bmp, &clip, false, &pipe, &src, 0, 0, &glyph);
597
598        assert_eq!(bmp.row(0)[0].r, 0, "row 0 should be clipped");
599        assert_eq!(bmp.row(1)[0].r, 0, "row 1 should be clipped");
600        assert_eq!(bmp.row(2)[2].r, 255, "(2,2) should be painted");
601    }
602
603    #[test]
604    fn fill_glyph_returns_clip_result() {
605        let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 4, false);
606        let clip = make_clip(8, 8);
607        let pipe = simple_pipe();
608        let color = [128u8, 128, 128];
609        let src = PipeSrc::Solid(&color);
610
611        let data = vec![128u8; 4];
612        let glyph = GlyphBitmap {
613            data: &data,
614            x: 0,
615            y: 0,
616            w: 2,
617            h: 2,
618            aa: true,
619        };
620
621        let res = fill_glyph::<Rgb8>(&mut bmp, &clip, &pipe, &src, 1, 1, &glyph);
622        assert_eq!(res, ClipResult::AllInside);
623    }
624
625    #[test]
626    fn glyph_row_bytes_aa() {
627        let data = [];
628        let g = GlyphBitmap {
629            data: &data,
630            x: 0,
631            y: 0,
632            w: 7,
633            h: 1,
634            aa: true,
635        };
636        assert_eq!(g.row_bytes(), 7);
637    }
638
639    #[test]
640    fn glyph_row_bytes_mono() {
641        let data = [];
642        let g = GlyphBitmap {
643            data: &data,
644            x: 0,
645            y: 0,
646            w: 7,
647            h: 1,
648            aa: false,
649        };
650        assert_eq!(g.row_bytes(), 1); // ceil(7/8) = 1
651        let g9 = GlyphBitmap {
652            data: &data,
653            x: 0,
654            y: 0,
655            w: 9,
656            h: 1,
657            aa: false,
658        };
659        assert_eq!(g9.row_bytes(), 2); // ceil(9/8) = 2
660    }
661}