Skip to main content

oxiui_render_soft/
blend.rs

1//! Extended compositing helpers: blend modes + premultiplied-alpha utilities.
2//!
3//! The default [`Framebuffer::blend`](crate::framebuffer::Framebuffer::blend)
4//! path uses *straight-alpha* source-over (Porter–Duff `over`). This module
5//! adds Photoshop / SVG / CSS classics — `multiply`, `screen`, `overlay`,
6//! `darken`, `lighten` — plus the premultiplied-alpha helpers callers need
7//! when they want to do bulk maths in linear-ish space and only de-multiply
8//! at the very end.
9//!
10//! All blend modes work on straight-alpha sRGB components in `[0, 255]`
11//! (i.e. exactly what the framebuffer stores), so they're additive: callers
12//! just pick a [`BlendMode`] and call [`blend_pixel`] (or
13//! [`composite_into`]) — `Framebuffer` itself is unchanged.
14//!
15//! ## Premultiplied-alpha pipeline
16//!
17//! The internal `blend_over_premult_u8` helper performs Porter–Duff
18//! source-over entirely in integer premultiplied-alpha space:
19//!   1. Convert source and destination from straight-alpha to premult (u16).
20//!   2. Blend in premult space: `out_r = src_pm + dst_pm * (255 - src_a) / 255`.
21//!   3. Convert the premultiplied result back to straight-alpha for storage.
22//!
23//! This avoids the per-pixel division inherent in straight-alpha compositing
24//! while keeping the public API and framebuffer format unchanged.
25
26use crate::framebuffer::{pack_rgba, unpack, Framebuffer};
27
28/// Compositing operator selected by [`blend_pixel`] /
29/// [`composite_into`].
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum BlendMode {
32    /// Standard Porter–Duff `source-over` (straight alpha).
33    Normal,
34    /// `C = src * dst` per colour channel (darkening filter).
35    Multiply,
36    /// `C = 1 - (1 - src) * (1 - dst)` (lightening filter).
37    Screen,
38    /// Per-channel: multiply when `dst < 0.5`, screen otherwise.
39    Overlay,
40    /// `C = min(src, dst)` per channel.
41    Darken,
42    /// `C = max(src, dst)` per channel.
43    Lighten,
44}
45
46/// A straight-alpha RGBA pixel as floats in `[0, 1]`.
47///
48/// Conversions to/from the framebuffer `0xAARRGGBB` format use the helpers
49/// at the bottom of this file (`to_unit` / `from_unit`).
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct RgbaUnit {
52    /// Red channel, `[0, 1]`.
53    pub r: f32,
54    /// Green channel, `[0, 1]`.
55    pub g: f32,
56    /// Blue channel, `[0, 1]`.
57    pub b: f32,
58    /// Alpha channel, `[0, 1]`.
59    pub a: f32,
60}
61
62impl RgbaUnit {
63    /// Construct a pixel from raw float components (clamped to `[0, 1]`).
64    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
65        Self {
66            r: r.clamp(0.0, 1.0),
67            g: g.clamp(0.0, 1.0),
68            b: b.clamp(0.0, 1.0),
69            a: a.clamp(0.0, 1.0),
70        }
71    }
72
73    /// Construct from an unsigned-byte `(r, g, b, a)` tuple.
74    pub fn from_bytes(r: u8, g: u8, b: u8, a: u8) -> Self {
75        Self {
76            r: r as f32 / 255.0,
77            g: g as f32 / 255.0,
78            b: b as f32 / 255.0,
79            a: a as f32 / 255.0,
80        }
81    }
82
83    /// Round to nearest `(r, g, b, a)` bytes.
84    pub fn to_bytes(self) -> (u8, u8, u8, u8) {
85        (
86            (self.r.clamp(0.0, 1.0) * 255.0).round() as u8,
87            (self.g.clamp(0.0, 1.0) * 255.0).round() as u8,
88            (self.b.clamp(0.0, 1.0) * 255.0).round() as u8,
89            (self.a.clamp(0.0, 1.0) * 255.0).round() as u8,
90        )
91    }
92
93    /// Multiply RGB by alpha — produces premultiplied form for fast bulk maths.
94    pub fn premultiply(self) -> Self {
95        Self {
96            r: self.r * self.a,
97            g: self.g * self.a,
98            b: self.b * self.a,
99            a: self.a,
100        }
101    }
102
103    /// Inverse of [`premultiply`](Self::premultiply); safe when `a == 0`
104    /// (returns transparent black rather than `NaN`).
105    pub fn unpremultiply(self) -> Self {
106        if self.a <= f32::EPSILON {
107            Self {
108                r: 0.0,
109                g: 0.0,
110                b: 0.0,
111                a: 0.0,
112            }
113        } else {
114            Self {
115                r: self.r / self.a,
116                g: self.g / self.a,
117                b: self.b / self.a,
118                a: self.a,
119            }
120        }
121    }
122}
123
124/// Convert a framebuffer packed pixel to floats in `[0, 1]`.
125pub fn to_unit(px: u32) -> RgbaUnit {
126    let (r, g, b, a) = unpack(px);
127    RgbaUnit::from_bytes(r, g, b, a)
128}
129
130/// Pack floats back into a framebuffer `0xAARRGGBB` value.
131pub fn from_unit(p: RgbaUnit) -> u32 {
132    let (r, g, b, a) = p.to_bytes();
133    pack_rgba(r, g, b, a)
134}
135
136// ---------------------------------------------------------------------------
137// Integer premultiplied-alpha pipeline helpers
138// ---------------------------------------------------------------------------
139
140/// Convert a straight-alpha pixel `(r, g, b, a)` to premultiplied-alpha `u16`
141/// components: `(r * a / 255, g * a / 255, b * a / 255, a)`.
142///
143/// Using `u16` avoids overflow: `255 * 255 = 65025 < u16::MAX`.
144#[inline]
145fn to_premult(r: u8, g: u8, b: u8, a: u8) -> (u16, u16, u16, u16) {
146    let a16 = a as u16;
147    (
148        (r as u16 * a16 + 127) / 255,
149        (g as u16 * a16 + 127) / 255,
150        (b as u16 * a16 + 127) / 255,
151        a16,
152    )
153}
154
155/// Convert premultiplied-alpha components `(pr, pg, pb, a)` back to
156/// straight-alpha bytes. Safe when `a == 0` (returns transparent black).
157#[inline]
158fn from_premult(pr: u16, pg: u16, pb: u16, a: u16) -> (u8, u8, u8, u8) {
159    if a == 0 {
160        return (0, 0, 0, 0);
161    }
162    let half = a / 2;
163    (
164        ((pr * 255 + half) / a).min(255) as u8,
165        ((pg * 255 + half) / a).min(255) as u8,
166        ((pb * 255 + half) / a).min(255) as u8,
167        a as u8,
168    )
169}
170
171/// Porter–Duff source-over in integer premultiplied-alpha space.
172///
173/// `src` and `dst` are straight-alpha `(r, g, b, a)` bytes.  The computation:
174///   1. Premultiply both.
175///   2. Blend: `out_r = src_pm + dst_pm * (255 - src_a) / 255`.
176///   3. Compute `out_a = src_a + dst_a * (255 - src_a) / 255`.
177///   4. Un-premultiply the result back to straight-alpha.
178///
179/// The framebuffer continues to store `0xAARRGGBB` straight-alpha — this
180/// function does not change the storage invariant.
181#[inline]
182#[allow(clippy::too_many_arguments)]
183pub(crate) fn blend_over_premult_u8(
184    src_r: u8,
185    src_g: u8,
186    src_b: u8,
187    src_a: u8,
188    dst_r: u8,
189    dst_g: u8,
190    dst_b: u8,
191    dst_a: u8,
192) -> (u8, u8, u8, u8) {
193    let (spr, spg, spb, sa) = to_premult(src_r, src_g, src_b, src_a);
194    let (dpr, dpg, dpb, da) = to_premult(dst_r, dst_g, dst_b, dst_a);
195    let inv_sa = 255u16 - sa;
196    let out_r = spr + (dpr * inv_sa + 127) / 255;
197    let out_g = spg + (dpg * inv_sa + 127) / 255;
198    let out_b = spb + (dpb * inv_sa + 127) / 255;
199    let out_a = sa + (da * inv_sa + 127) / 255;
200    from_premult(out_r, out_g, out_b, out_a)
201}
202
203/// Apply `mode` to compose `src` over `dst`, returning the result in
204/// straight-alpha float form.
205///
206/// All non-`Normal` modes use the standard SVG-compositing recipe:
207/// the per-channel colour blend function `B(Cb, Cs)` is computed, then
208/// combined with the alphas:
209///
210/// ```text
211///   Co = (1 - dst.a) * Cs + (1 - src.a) * Cb + src.a * dst.a * B(Cb, Cs)
212///   Ao = src.a + dst.a * (1 - src.a)
213/// ```
214///
215/// This matches SVG 1.1 "Composite + Blend" semantics and CSS
216/// `background-blend-mode`.
217pub fn blend_mode(mode: BlendMode, src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
218    match mode {
219        BlendMode::Normal => over(src, dst),
220        BlendMode::Multiply => mode_combine(src, dst, |a, b| a * b),
221        BlendMode::Screen => mode_combine(src, dst, |a, b| a + b - a * b),
222        BlendMode::Overlay => mode_combine(src, dst, |cs, cb| {
223            // SVG: overlay(Cb, Cs) = hard-light(Cs, Cb).
224            if cb <= 0.5 {
225                2.0 * cs * cb
226            } else {
227                1.0 - 2.0 * (1.0 - cs) * (1.0 - cb)
228            }
229        }),
230        BlendMode::Darken => mode_combine(src, dst, f32::min),
231        BlendMode::Lighten => mode_combine(src, dst, f32::max),
232    }
233}
234
235/// Plain Porter–Duff source-over in straight-alpha space.
236fn over(src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
237    let out_a = src.a + dst.a * (1.0 - src.a);
238    if out_a <= f32::EPSILON {
239        return RgbaUnit {
240            r: 0.0,
241            g: 0.0,
242            b: 0.0,
243            a: 0.0,
244        };
245    }
246    let blend = |s: f32, d: f32| (s * src.a + d * dst.a * (1.0 - src.a)) / out_a;
247    RgbaUnit {
248        r: blend(src.r, dst.r),
249        g: blend(src.g, dst.g),
250        b: blend(src.b, dst.b),
251        a: out_a,
252    }
253}
254
255/// Generic SVG-style blend: applies `blend_fn` per colour channel and
256/// composes the result with proper alpha handling.
257fn mode_combine<F>(src: RgbaUnit, dst: RgbaUnit, blend_fn: F) -> RgbaUnit
258where
259    F: Fn(f32, f32) -> f32,
260{
261    let out_a = src.a + dst.a * (1.0 - src.a);
262    if out_a <= f32::EPSILON {
263        return RgbaUnit {
264            r: 0.0,
265            g: 0.0,
266            b: 0.0,
267            a: 0.0,
268        };
269    }
270    // Premultiplied per the SVG compositing equation.
271    let channel = |cs: f32, cb: f32| -> f32 {
272        let b = blend_fn(cs.clamp(0.0, 1.0), cb.clamp(0.0, 1.0));
273        let v = (1.0 - dst.a) * cs * src.a + (1.0 - src.a) * cb * dst.a + src.a * dst.a * b;
274        // De-premultiply by out_a so the framebuffer stays in straight-alpha form.
275        (v / out_a).clamp(0.0, 1.0)
276    };
277    RgbaUnit {
278        r: channel(src.r, dst.r),
279        g: channel(src.g, dst.g),
280        b: channel(src.b, dst.b),
281        a: out_a,
282    }
283}
284
285/// Apply `mode` to blend `src` over the pixel currently at `(x, y)` in `fb`.
286/// Out-of-bounds writes are ignored. For `Normal` this is equivalent to
287/// [`Framebuffer::blend`](crate::framebuffer::Framebuffer::blend).
288pub fn blend_pixel(fb: &mut Framebuffer, x: u32, y: u32, src: RgbaUnit, mode: BlendMode) {
289    if x >= fb.width() || y >= fb.height() {
290        return;
291    }
292    let Some(dst_px) = fb.get(x, y) else { return };
293    let dst = to_unit(dst_px);
294    let out = blend_mode(mode, src, dst);
295    fb.set(x, y, from_unit(out));
296}
297
298/// Composite a rectangular `src` slab (interpreted as straight-alpha
299/// RGBA8 row-major, `w * h * 4` bytes) into `fb` at `(dst_x, dst_y)`
300/// using `mode`. Returns the number of pixels written.
301///
302/// For [`BlendMode::Normal`] the composite is done in integer premultiplied-
303/// alpha space (via `blend_over_premult_u8`) so the blend math is exact and
304/// avoids the per-pixel float division. Other modes fall back to the f32 path.
305pub fn composite_into(
306    fb: &mut Framebuffer,
307    src: &[u8],
308    w: u32,
309    h: u32,
310    dst_x: i64,
311    dst_y: i64,
312    mode: BlendMode,
313) -> usize {
314    if w == 0 || h == 0 || src.len() < (w * h * 4) as usize {
315        return 0;
316    }
317    let mut written = 0;
318    for j in 0..h {
319        for i in 0..w {
320            let si = ((j * w + i) * 4) as usize;
321            let r = src[si];
322            let g = src[si + 1];
323            let b = src[si + 2];
324            let a = src[si + 3];
325            if a == 0 {
326                continue;
327            }
328            let px = dst_x + i as i64;
329            let py = dst_y + j as i64;
330            if px < 0 || py < 0 {
331                continue;
332            }
333            let pu = px as u32;
334            let pv = py as u32;
335            if pu >= fb.width() || pv >= fb.height() {
336                continue;
337            }
338            if mode == BlendMode::Normal {
339                // Fast path: premultiplied-alpha source-over in integer space.
340                // Framebuffer stays in straight-alpha 0xAARRGGBB.
341                let dst_px = fb.get(pu, pv).unwrap_or(0);
342                let (dr, dg, db, da) = unpack(dst_px);
343                let (or_, og, ob, oa) = blend_over_premult_u8(r, g, b, a, dr, dg, db, da);
344                fb.set(pu, pv, crate::framebuffer::pack_rgba(or_, og, ob, oa));
345            } else {
346                blend_pixel(fb, pu, pv, RgbaUnit::from_bytes(r, g, b, a), mode);
347            }
348            written += 1;
349        }
350    }
351    written
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use oxiui_core::Color;
358
359    fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
360        (a - b).abs() <= eps
361    }
362
363    #[test]
364    fn round_trip_unit_bytes() {
365        let p = RgbaUnit::from_bytes(123, 45, 200, 99);
366        let (r, g, b, a) = p.to_bytes();
367        assert_eq!((r, g, b, a), (123, 45, 200, 99));
368    }
369
370    #[test]
371    fn premultiply_inverse() {
372        let p = RgbaUnit::new(0.8, 0.4, 0.2, 0.5);
373        let q = p.premultiply().unpremultiply();
374        assert!(approx_eq(q.r, p.r, 1e-5));
375        assert!(approx_eq(q.g, p.g, 1e-5));
376        assert!(approx_eq(q.b, p.b, 1e-5));
377        assert!(approx_eq(q.a, p.a, 1e-5));
378    }
379
380    #[test]
381    fn premultiply_zero_alpha_safe() {
382        let p = RgbaUnit::new(0.9, 0.9, 0.9, 0.0);
383        let q = p.premultiply().unpremultiply();
384        assert!(approx_eq(q.a, 0.0, 1e-5));
385        assert!(approx_eq(q.r, 0.0, 1e-5));
386    }
387
388    #[test]
389    fn multiply_white_is_identity() {
390        // src=white opaque, mode=Multiply over red dst → red preserved.
391        let src = RgbaUnit::from_bytes(255, 255, 255, 255);
392        let dst = RgbaUnit::from_bytes(255, 0, 0, 255);
393        let out = blend_mode(BlendMode::Multiply, src, dst);
394        let (r, g, b, a) = out.to_bytes();
395        assert_eq!((r, g, b, a), (255, 0, 0, 255));
396    }
397
398    #[test]
399    fn screen_black_is_identity() {
400        let src = RgbaUnit::from_bytes(0, 0, 0, 255);
401        let dst = RgbaUnit::from_bytes(80, 120, 200, 255);
402        let out = blend_mode(BlendMode::Screen, src, dst);
403        let (r, g, b, _) = out.to_bytes();
404        // Screen with black should keep dst exactly.
405        assert!((r as i32 - 80).abs() <= 1);
406        assert!((g as i32 - 120).abs() <= 1);
407        assert!((b as i32 - 200).abs() <= 1);
408    }
409
410    #[test]
411    fn darken_picks_min_per_channel() {
412        let src = RgbaUnit::from_bytes(100, 200, 50, 255);
413        let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
414        let out = blend_mode(BlendMode::Darken, src, dst);
415        let (r, g, b, _) = out.to_bytes();
416        assert!((r as i32 - 100).abs() <= 1);
417        assert!((g as i32 - 50).abs() <= 1);
418        assert!((b as i32 - 50).abs() <= 1);
419    }
420
421    #[test]
422    fn lighten_picks_max_per_channel() {
423        let src = RgbaUnit::from_bytes(100, 200, 50, 255);
424        let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
425        let out = blend_mode(BlendMode::Lighten, src, dst);
426        let (r, g, b, _) = out.to_bytes();
427        assert!((r as i32 - 150).abs() <= 1);
428        assert!((g as i32 - 200).abs() <= 1);
429        assert!((b as i32 - 50).abs() <= 1);
430    }
431
432    #[test]
433    fn overlay_changes_with_dst_mid() {
434        // Dst at 0.5 → overlay is the boundary; verify it produces some
435        // smooth blend with a grey src (no NaN / no panic).
436        let src = RgbaUnit::from_bytes(128, 128, 128, 255);
437        let dst = RgbaUnit::from_bytes(128, 128, 128, 255);
438        let out = blend_mode(BlendMode::Overlay, src, dst);
439        let (r, _, _, a) = out.to_bytes();
440        assert_eq!(a, 255);
441        // Result must remain in-gamut (r is u8, so always <= 255; just verify
442        // we got a valid result).
443        let _ = r;
444    }
445
446    #[test]
447    fn over_zero_alpha_safe() {
448        let src = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
449        let dst = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
450        let out = blend_mode(BlendMode::Normal, src, dst);
451        let (_, _, _, a) = out.to_bytes();
452        assert_eq!(a, 0);
453    }
454
455    #[test]
456    fn composite_into_writes_pixels() {
457        let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
458        // 2x2 all-white opaque src.
459        let src = vec![
460            255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
461        ];
462        let written = composite_into(&mut fb, &src, 2, 2, 1, 1, BlendMode::Normal);
463        assert_eq!(written, 4);
464        let result = fb.get_rgba(1, 1).unwrap_or_default();
465        assert_eq!(result, (255, 255, 255, 255));
466    }
467
468    #[test]
469    fn blend_pixel_out_of_bounds_noop() {
470        let mut fb = Framebuffer::with_fill(2, 2, Color(10, 20, 30, 255));
471        blend_pixel(
472            &mut fb,
473            99,
474            99,
475            RgbaUnit::from_bytes(255, 255, 255, 255),
476            BlendMode::Normal,
477        );
478        let v = fb.get_rgba(0, 0).unwrap_or((0, 0, 0, 0));
479        assert_eq!(v, (10, 20, 30, 255));
480    }
481
482    #[test]
483    fn blend_multiply_black_is_black() {
484        // Multiply any colour with opaque black → black.
485        let black = RgbaUnit::from_bytes(0, 0, 0, 255);
486        let src = RgbaUnit::from_bytes(200, 100, 50, 255);
487        let out = blend_mode(BlendMode::Multiply, src, black);
488        let (r, g, b, _) = out.to_bytes();
489        assert_eq!((r, g, b), (0, 0, 0), "multiply with black must give black");
490    }
491
492    #[test]
493    fn blend_screen_white_is_white() {
494        // Screen any colour with opaque white → white.
495        let white = RgbaUnit::from_bytes(255, 255, 255, 255);
496        let src = RgbaUnit::from_bytes(100, 150, 200, 255);
497        let out = blend_mode(BlendMode::Screen, src, white);
498        let (r, g, b, _) = out.to_bytes();
499        assert_eq!(
500            (r, g, b),
501            (255, 255, 255),
502            "screen with white must give white"
503        );
504    }
505
506    #[test]
507    fn premultiply_roundtrip() {
508        // Premultiply then unpremultiply should recover the original (within rounding).
509        let p = RgbaUnit::new(0.7, 0.3, 0.9, 0.6);
510        let q = p.premultiply().unpremultiply();
511        assert!(approx_eq(q.r, p.r, 1e-4), "r: {:.5} vs {:.5}", q.r, p.r);
512        assert!(approx_eq(q.g, p.g, 1e-4), "g: {:.5} vs {:.5}", q.g, p.g);
513        assert!(approx_eq(q.b, p.b, 1e-4), "b: {:.5} vs {:.5}", q.b, p.b);
514        assert!(approx_eq(q.a, p.a, 1e-5), "a: {:.5} vs {:.5}", q.a, p.a);
515    }
516
517    // -----------------------------------------------------------------------
518    // S2: premultiplied-alpha golden-value + signature-stability tests
519    // -----------------------------------------------------------------------
520
521    #[test]
522    fn test_premult_blend_golden() {
523        // Blend semi-transparent red (r=255, g=0, b=0, a=128) over opaque white
524        // (r=255, g=255, b=255, a=255).
525        //
526        // Expected result (straight-alpha source-over):
527        //   out_a = 128 + 255 * (255 - 128) / 255  = 128 + 127 = 255
528        //   out_r = (255 * 128 + 255 * 255 * 127/255) / 255
529        //         = (32640 + 32385) / 255 ≈ 255
530        //   out_g = (0   * 128 + 255 * 127) / 255 ≈ 127
531        //   out_b = (0   * 128 + 255 * 127) / 255 ≈ 127
532        //
533        // Golden values: (255, 127, 127, 255), tolerance ±2.
534        let (r, g, b, a) = blend_over_premult_u8(255, 0, 0, 128, 255, 255, 255, 255);
535        assert!((r as i32 - 255).abs() <= 2, "r={r} expected ~255");
536        assert!((g as i32 - 127).abs() <= 2, "g={g} expected ~127");
537        assert!((b as i32 - 127).abs() <= 2, "b={b} expected ~127");
538        assert!((a as i32 - 255).abs() <= 2, "a={a} expected ~255");
539    }
540
541    #[test]
542    fn test_blend_signatures_stable() {
543        // Compile-time + runtime stability check: call every public blend fn
544        // with valid args and verify it does not panic.
545
546        // blend_mode
547        let src = RgbaUnit::from_bytes(128, 64, 32, 200);
548        let dst = RgbaUnit::from_bytes(10, 20, 30, 255);
549        let _ = blend_mode(BlendMode::Normal, src, dst);
550        let _ = blend_mode(BlendMode::Multiply, src, dst);
551        let _ = blend_mode(BlendMode::Screen, src, dst);
552        let _ = blend_mode(BlendMode::Overlay, src, dst);
553        let _ = blend_mode(BlendMode::Darken, src, dst);
554        let _ = blend_mode(BlendMode::Lighten, src, dst);
555
556        // to_unit / from_unit round-trip
557        let px = crate::framebuffer::pack_rgba(100, 150, 200, 180);
558        let unit = to_unit(px);
559        let _ = from_unit(unit);
560
561        // blend_pixel (out-of-bounds is a no-op; just verify no panic)
562        let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
563        blend_pixel(&mut fb, 0, 0, src, BlendMode::Normal);
564        blend_pixel(&mut fb, 99, 99, src, BlendMode::Screen); // out-of-bounds noop
565
566        // composite_into (minimal 1×1)
567        let src_bytes = [255u8, 0, 0, 128];
568        let written = composite_into(&mut fb, &src_bytes, 1, 1, 0, 0, BlendMode::Normal);
569        assert!(written <= 1);
570    }
571}