ironrdp_session/
image.rs

1use std::sync::Arc;
2
3use ironrdp_core::assert_impl;
4use ironrdp_graphics::color_conversion::rdp_16bit_to_rgb;
5use ironrdp_graphics::image_processing::{ImageRegion, ImageRegionMut, PixelFormat};
6use ironrdp_graphics::pointer::DecodedPointer;
7use ironrdp_graphics::rectangle_processing::Region;
8use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
9use tracing::trace;
10
11use crate::{custom_err, SessionResult};
12
13const TILE_SIZE: u16 = 64;
14
15pub struct DecodedImage {
16    pixel_format: PixelFormat,
17    data: Vec<u8>,
18
19    /// Part of the pointer image which should be drawn
20    pointer_src_rect: InclusiveRectangle,
21    /// X position of the pointer sprite on the screen
22    pointer_draw_x: u16,
23    /// Y position of the pointer sprite on the screen
24    pointer_draw_y: u16,
25
26    pointer_x: u16,
27    pointer_y: u16,
28
29    pointer: Option<Arc<DecodedPointer>>,
30    /// Image data, overridden by pointer. Used to restore image after pointer was hidden or moved
31    pointer_backbuffer: Vec<u8>,
32    /// Whether to show pointer or not
33    show_pointer: bool,
34    /// Whether pointer is visible on the screen or its sprite is currently out of bounds
35    pointer_visible_on_screen: bool,
36
37    width: u16,
38    height: u16,
39}
40
41assert_impl!(DecodedImage: Send);
42
43impl core::fmt::Debug for DecodedImage {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        f.debug_struct("DecodedImage")
46            .field("pixel_format", &self.pixel_format)
47            .field("data_len", &self.data.len())
48            .field("pointer_src_rect", &self.pointer_src_rect)
49            .field("pointer_draw_x", &self.pointer_draw_x)
50            .field("pointer_draw_y", &self.pointer_draw_y)
51            .field("pointer_x", &self.pointer_x)
52            .field("pointer_y", &self.pointer_y)
53            .field("pointer", &self.pointer)
54            .field("pointer_backbuffer", &self.pointer_backbuffer)
55            .field("show_pointer", &self.show_pointer)
56            .field("pointer_visible_on_screen", &self.pointer_visible_on_screen)
57            .field("width", &self.width)
58            .field("height", &self.height)
59            .finish()
60    }
61}
62
63#[derive(PartialEq, Eq)]
64enum PointerLayer {
65    Background,
66    Pointer,
67}
68
69struct PointerRenderingState {
70    redraw: bool,
71    update_rectangle: InclusiveRectangle,
72}
73
74#[expect(clippy::too_many_arguments)]
75fn copy_cursor_data(
76    from: &[u8],
77    from_pos: (usize, usize),
78    from_stride: usize,
79    to: &mut [u8],
80    to_stride: usize,
81    to_pos: (usize, usize),
82    size: (usize, usize),
83    dst_size: (usize, usize),
84    composite: bool,
85) {
86    const PIXEL_SIZE: usize = 4;
87
88    if to_pos.0 + size.0 > dst_size.0 || to_pos.1 + size.1 > dst_size.1 {
89        // Perform clipping
90        return;
91    }
92
93    let (from_x, from_y) = from_pos;
94    let (to_x, to_y) = to_pos;
95    let (width, height) = size;
96
97    for y in 0..height {
98        let from_start = (from_y + y) * from_stride + from_x * PIXEL_SIZE;
99        let to_start = (to_y + y) * to_stride + to_x * PIXEL_SIZE;
100
101        if composite {
102            for pixel in 0..width {
103                let dest_r = to[to_start + pixel * PIXEL_SIZE];
104                let dest_g = to[to_start + pixel * PIXEL_SIZE + 1];
105                let dest_b = to[to_start + pixel * PIXEL_SIZE + 2];
106
107                let src_r = from[from_start + pixel * PIXEL_SIZE];
108                let src_g = from[from_start + pixel * PIXEL_SIZE + 1];
109                let src_b = from[from_start + pixel * PIXEL_SIZE + 2];
110                let src_a = from[from_start + pixel * PIXEL_SIZE + 3];
111
112                // Inverted pixel, this color has a special meaning when encoded by ironrdp-graphics
113                if src_a == 0 && src_r == 255 && src_g == 255 && src_b == 255 {
114                    to[to_start + pixel * PIXEL_SIZE] = 255 - dest_r;
115                    to[to_start + pixel * PIXEL_SIZE + 1] = 255 - dest_g;
116                    to[to_start + pixel * PIXEL_SIZE + 2] = 255 - dest_b;
117                    to[to_start + pixel * PIXEL_SIZE + 3] = 255;
118                    continue;
119                }
120
121                // Skip 100% transparent pixels
122                if src_a == 0 {
123                    continue;
124                }
125
126                #[expect(clippy::as_conversions, reason = "(u16 >> 8) fits into u8 + hot loop")]
127                {
128                    // Integer alpha blending, source represented as premultiplied alpha color, calculation in floating point
129                    to[to_start + pixel * PIXEL_SIZE] =
130                        src_r + ((u16::from(dest_r) * u16::from(255 - src_a)) >> 8) as u8;
131                    to[to_start + pixel * PIXEL_SIZE + 1] =
132                        src_g + ((u16::from(dest_g) * u16::from(255 - src_a)) >> 8) as u8;
133                    to[to_start + pixel * PIXEL_SIZE + 2] =
134                        src_b + ((u16::from(dest_b) * u16::from(255 - src_a)) >> 8) as u8;
135                    // Framebuffer is always opaque, so we can skip alpha channel change
136                }
137            }
138        } else {
139            to[to_start..to_start + width * PIXEL_SIZE]
140                .copy_from_slice(&from[from_start..from_start + width * PIXEL_SIZE]);
141        }
142    }
143}
144
145impl DecodedImage {
146    pub fn new(pixel_format: PixelFormat, width: u16, height: u16) -> Self {
147        let len = usize::from(width) * usize::from(height) * usize::from(pixel_format.bytes_per_pixel());
148
149        Self {
150            pixel_format,
151            data: vec![0; len],
152            width,
153            height,
154
155            pointer_src_rect: InclusiveRectangle {
156                left: 0,
157                top: 0,
158                right: 0,
159                bottom: 0,
160            },
161            pointer_x: 0,
162            pointer_y: 0,
163            pointer_draw_x: 0,
164            pointer_draw_y: 0,
165            pointer_backbuffer: Vec::new(),
166            pointer: None,
167            show_pointer: false,
168            pointer_visible_on_screen: true,
169        }
170    }
171
172    pub fn pixel_format(&self) -> PixelFormat {
173        self.pixel_format
174    }
175
176    pub fn data(&self) -> &[u8] {
177        &self.data
178    }
179
180    pub fn width(&self) -> u16 {
181        self.width
182    }
183
184    pub fn bytes_per_pixel(&self) -> usize {
185        usize::from(self.pixel_format.bytes_per_pixel())
186    }
187
188    pub fn stride(&self) -> usize {
189        usize::from(self.width) * self.bytes_per_pixel()
190    }
191
192    pub fn data_for_rect(&self, rect: &InclusiveRectangle) -> &[u8] {
193        let start = usize::from(rect.left) * self.bytes_per_pixel() + usize::from(rect.top) * self.stride();
194        let end =
195            start + usize::from(rect.height() - 1) * self.stride() + usize::from(rect.width()) * self.bytes_per_pixel();
196        &self.data[start..end]
197    }
198
199    pub fn height(&self) -> u16 {
200        self.height
201    }
202
203    fn apply_pointer_layer(&mut self, layer: PointerLayer) -> SessionResult<Option<InclusiveRectangle>> {
204        // Pointer is not hidden, but its texture is not visible on the screen, so we don't
205        // need to render it
206        if layer == PointerLayer::Pointer && !self.pointer_visible_on_screen {
207            return Ok(None);
208        }
209
210        if self.data.is_empty() {
211            return Ok(None);
212        }
213
214        let pointer = if let Some(pointer) = &self.pointer {
215            pointer
216        } else {
217            return Ok(None);
218        };
219
220        if self.pointer_src_rect.width() == 0 || self.pointer_src_rect.height() == 0 {
221            return Ok(None);
222        }
223
224        let dest_rect = InclusiveRectangle {
225            left: self.pointer_draw_x,
226            top: self.pointer_draw_y,
227            right: self.pointer_draw_x + self.pointer_src_rect.width() - 1,
228            bottom: self.pointer_draw_y + self.pointer_src_rect.height() - 1,
229        };
230
231        if dest_rect.width() == 0 || dest_rect.height() == 0 {
232            return Ok(None);
233        }
234
235        let pointer_src_rect_width = usize::from(self.pointer_src_rect.width());
236        let pointer_src_rect_height = usize::from(self.pointer_src_rect.height());
237        let pointer_draw_x = usize::from(self.pointer_draw_x);
238        let pointer_draw_y = usize::from(self.pointer_draw_y);
239        let width = usize::from(self.width);
240        let height = usize::from(self.height);
241
242        match &layer {
243            PointerLayer::Background => {
244                if self.pointer_backbuffer.is_empty() {
245                    // Backbuffer were previously empty
246                    return Ok(None);
247                }
248
249                copy_cursor_data(
250                    &self.pointer_backbuffer,
251                    (0, 0),
252                    pointer_src_rect_width * 4,
253                    &mut self.data,
254                    width * 4,
255                    (pointer_draw_x, pointer_draw_y),
256                    (pointer_src_rect_width, pointer_src_rect_height),
257                    (width, height),
258                    false,
259                );
260            }
261            PointerLayer::Pointer => {
262                // Copy current background to backbuffer
263                let buffer_size = self
264                    .pointer_backbuffer
265                    .len()
266                    .max(pointer_src_rect_width * pointer_src_rect_height * 4);
267                self.pointer_backbuffer.resize(buffer_size, 0);
268
269                copy_cursor_data(
270                    &self.data,
271                    (pointer_draw_x, pointer_draw_y),
272                    width * 4,
273                    &mut self.pointer_backbuffer,
274                    pointer_src_rect_width * 4,
275                    (0, 0),
276                    (pointer_src_rect_width, pointer_src_rect_height),
277                    (width, height),
278                    false,
279                );
280
281                // Draw pointer (with compositing)
282                copy_cursor_data(
283                    pointer.bitmap_data.as_slice(),
284                    (
285                        usize::from(self.pointer_src_rect.left),
286                        usize::from(self.pointer_src_rect.top),
287                    ),
288                    usize::from(pointer.width) * 4,
289                    &mut self.data,
290                    width * 4,
291                    (pointer_draw_x, pointer_draw_y),
292                    (pointer_src_rect_width, pointer_src_rect_height),
293                    (width, height),
294                    true,
295                );
296            }
297        }
298
299        // Request redraw of the changed area
300        Ok(Some(dest_rect))
301    }
302
303    pub(crate) fn show_pointer(&mut self) -> SessionResult<Option<InclusiveRectangle>> {
304        if !self.show_pointer {
305            self.show_pointer = true;
306            self.apply_pointer_layer(PointerLayer::Pointer)
307        } else {
308            Ok(None)
309        }
310    }
311
312    pub(crate) fn hide_pointer(&mut self) -> SessionResult<Option<InclusiveRectangle>> {
313        if self.show_pointer {
314            self.show_pointer = false;
315            self.apply_pointer_layer(PointerLayer::Background)
316        } else {
317            Ok(None)
318        }
319    }
320
321    fn recalculate_pointer_geometry(&mut self) {
322        let x = self.pointer_x;
323        let y = self.pointer_y;
324
325        let pointer = match &self.pointer {
326            Some(pointer) if self.show_pointer => pointer,
327            _ => return,
328        };
329
330        let left_virtual = i32::from(x) - i32::from(pointer.hotspot_x);
331        let top_virtual = i32::from(y) - i32::from(pointer.hotspot_y);
332        let right_virtual = left_virtual + i32::from(pointer.width) - 1;
333        let bottom_virtual = top_virtual + i32::from(pointer.height) - 1;
334
335        let (left, draw_x) = if left_virtual < 0 {
336            // Cut left side if required
337            (pointer.hotspot_x - x, 0)
338        } else {
339            (0, x - pointer.hotspot_x)
340        };
341
342        let (top, draw_y) = if top_virtual < 0 {
343            // Cut top side if required
344            (pointer.hotspot_y - y, 0)
345        } else {
346            (0, y - pointer.hotspot_y)
347        };
348
349        // Cut right side if required
350        let right = if right_virtual >= i32::from(self.width - 1) {
351            if draw_x + 1 >= self.width {
352                // Pointer is completely out of bounds horizontally
353                self.pointer_visible_on_screen = false;
354                return;
355            } else {
356                self.width - (draw_x + 1)
357            }
358        } else {
359            pointer.width - 1
360        };
361
362        // Cut bottom side if required
363        let bottom = if bottom_virtual >= i32::from(self.height - 1) {
364            if (draw_y + 1) >= self.height {
365                // Pointer is completely out of bounds vertically
366                self.pointer_visible_on_screen = false;
367                return;
368            } else {
369                self.height - (draw_y + 1)
370            }
371        } else {
372            pointer.height - 1
373        };
374
375        self.pointer_visible_on_screen = true;
376
377        let pointer_src_rect = InclusiveRectangle {
378            left,
379            top,
380            right,
381            bottom,
382        };
383
384        self.pointer_src_rect = pointer_src_rect;
385        self.pointer_draw_x = draw_x;
386        self.pointer_draw_y = draw_y;
387    }
388
389    pub(crate) fn move_pointer(&mut self, x: u16, y: u16) -> SessionResult<Option<InclusiveRectangle>> {
390        self.pointer_x = x;
391        self.pointer_y = y;
392
393        if self.pointer.is_some() && self.show_pointer {
394            let old_rect = self.apply_pointer_layer(PointerLayer::Background)?;
395            self.recalculate_pointer_geometry();
396            let new_rect = self.apply_pointer_layer(PointerLayer::Pointer)?;
397
398            match (old_rect, new_rect) {
399                (None, None) => Ok(None),
400                (None, Some(rect)) => Ok(Some(rect)),
401                (Some(rect), None) => Ok(Some(rect)),
402                (Some(a), Some(b)) => Ok(Some(a.union(&b))),
403            }
404        } else {
405            Ok(None)
406        }
407    }
408
409    pub(crate) fn update_pointer(&mut self, pointer: Arc<DecodedPointer>) -> SessionResult<Option<InclusiveRectangle>> {
410        self.show_pointer = true;
411
412        // Remove old pointer from frame buffer
413        let old_rect = if self.pointer.is_some() {
414            self.apply_pointer_layer(PointerLayer::Background)?
415        } else {
416            None
417        };
418
419        self.pointer = Some(pointer);
420        self.recalculate_pointer_geometry();
421
422        // Draw new pointer
423        let new_rect = self.apply_pointer_layer(PointerLayer::Pointer)?;
424
425        match (old_rect, new_rect) {
426            (None, None) => Ok(None),
427            (None, Some(rect)) => Ok(Some(rect)),
428            (Some(rect), None) => Ok(Some(rect)),
429            (Some(a), Some(b)) => Ok(Some(a.union(&b))),
430        }
431    }
432
433    fn is_pointer_redraw_required(&self, update_rectangle: &InclusiveRectangle) -> bool {
434        let pointer_dest_rect = InclusiveRectangle {
435            left: self.pointer_draw_x,
436            top: self.pointer_draw_y,
437            right: self.pointer_draw_x + self.pointer_src_rect.width() - 1,
438            bottom: self.pointer_draw_y + self.pointer_src_rect.height() - 1,
439        };
440
441        update_rectangle.intersect(&pointer_dest_rect).is_some() && self.show_pointer
442    }
443
444    /// This method should be called BEFORE and framebuffer updates, with the update rectangle,
445    /// to determine if the pointer needs to be redrawn (overlapping with the update rectangle).
446    fn pointer_rendering_begin(
447        &mut self,
448        update_rectangle: &InclusiveRectangle,
449    ) -> SessionResult<PointerRenderingState> {
450        if !self.is_pointer_redraw_required(update_rectangle) || self.pointer.is_none() {
451            return Ok(PointerRenderingState {
452                redraw: false,
453                update_rectangle: update_rectangle.clone(),
454            });
455        }
456
457        let state = self
458            .apply_pointer_layer(PointerLayer::Background)?
459            .map(|cursor_erase_rect| PointerRenderingState {
460                redraw: true,
461                update_rectangle: cursor_erase_rect.union(update_rectangle),
462            })
463            .unwrap_or_else(|| PointerRenderingState {
464                redraw: false,
465                update_rectangle: update_rectangle.clone(),
466            });
467
468        Ok(state)
469    }
470
471    fn pointer_rendering_end(
472        &mut self,
473        pointer_rendering_state: PointerRenderingState,
474    ) -> SessionResult<InclusiveRectangle> {
475        if !pointer_rendering_state.redraw {
476            return Ok(pointer_rendering_state.update_rectangle);
477        }
478
479        let update_rectangle = self
480            .apply_pointer_layer(PointerLayer::Pointer)?
481            .map(|pointer_draw_rectangle| pointer_draw_rectangle.union(&pointer_rendering_state.update_rectangle))
482            .unwrap_or_else(|| pointer_rendering_state.update_rectangle);
483
484        Ok(update_rectangle)
485    }
486
487    // To apply the buffer, we need to un-apply previously drawn cursor, and then apply it again
488    // in other position.
489
490    pub(crate) fn apply_tile(
491        &mut self,
492        tile_output: &[u8],
493        pixel_format: PixelFormat,
494        clipping_rectangles: &Region,
495        update_rectangle: &InclusiveRectangle,
496    ) -> SessionResult<InclusiveRectangle> {
497        trace!("Tile: {:?}", update_rectangle);
498
499        let pointer_rendering_state = self.pointer_rendering_begin(&clipping_rectangles.extents)?;
500
501        let update_region = clipping_rectangles.intersect_rectangle(update_rectangle);
502        for region_rectangle in &update_region.rectangles {
503            let source_x = region_rectangle.left - update_rectangle.left;
504            let source_y = region_rectangle.top - update_rectangle.top;
505            let stride = u16::from(pixel_format.bytes_per_pixel()) * TILE_SIZE;
506            let source_image_region = ImageRegion {
507                region: InclusiveRectangle {
508                    left: source_x,
509                    top: source_y,
510                    right: source_x + region_rectangle.width() - 1,
511                    bottom: source_y + region_rectangle.height() - 1,
512                },
513                data: tile_output,
514                step: stride,
515                pixel_format,
516            };
517
518            let mut destination_image_region = ImageRegionMut {
519                region: region_rectangle.clone(),
520                step: self.width() * u16::from(self.pixel_format.bytes_per_pixel()),
521                pixel_format: self.pixel_format,
522                data: &mut self.data,
523            };
524
525            trace!("Source image region: {:?}", source_image_region.region);
526            trace!("Destination image region: {:?}", destination_image_region.region);
527
528            source_image_region
529                .copy_to(&mut destination_image_region)
530                .map_err(|e| custom_err!("copy_to", e))?;
531        }
532
533        let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?;
534
535        Ok(update_rectangle)
536    }
537
538    // FIXME: this assumes PixelFormat::RgbA32
539    pub(crate) fn apply_rgb16_bitmap(
540        &mut self,
541        rgb16: &[u8],
542        update_rectangle: &InclusiveRectangle,
543    ) -> SessionResult<InclusiveRectangle> {
544        const SRC_COLOR_DEPTH: usize = 2;
545        const DST_COLOR_DEPTH: usize = 4;
546
547        let image_width = usize::from(self.width);
548        let rectangle_width = usize::from(update_rectangle.width());
549        let top = usize::from(update_rectangle.top);
550        let left = usize::from(update_rectangle.left);
551
552        let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?;
553
554        rgb16
555            .chunks_exact(rectangle_width * SRC_COLOR_DEPTH)
556            .rev()
557            .enumerate()
558            .for_each(|(row_idx, row)| {
559                row.chunks_exact(SRC_COLOR_DEPTH)
560                    .enumerate()
561                    .for_each(|(col_idx, src_pixel)| {
562                        let rgb16_value = u16::from_le_bytes(
563                            src_pixel
564                                .try_into()
565                                .expect("src_pixel contains exactly two u8 elements"),
566                        );
567                        let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH;
568
569                        let [r, g, b] = rdp_16bit_to_rgb(rgb16_value);
570                        self.data[dst_idx] = r;
571                        self.data[dst_idx + 1] = g;
572                        self.data[dst_idx + 2] = b;
573                        self.data[dst_idx + 3] = 0xff;
574                    })
575            });
576
577        let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?;
578
579        Ok(update_rectangle)
580    }
581
582    // FIXME: this assumes PixelFormat::RgbA32
583    fn apply_rgb24_iter<'a, I>(
584        &mut self,
585        rgb24: I,
586        update_rectangle: &InclusiveRectangle,
587    ) -> SessionResult<InclusiveRectangle>
588    where
589        I: Iterator<Item = &'a [u8]>,
590    {
591        const SRC_COLOR_DEPTH: usize = 3;
592        const DST_COLOR_DEPTH: usize = 4;
593
594        let image_width = usize::from(self.width);
595        let top = usize::from(update_rectangle.top);
596        let left = usize::from(update_rectangle.left);
597
598        let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?;
599
600        rgb24.enumerate().for_each(|(row_idx, row)| {
601            row.chunks_exact(SRC_COLOR_DEPTH)
602                .enumerate()
603                .for_each(|(col_idx, src_pixel)| {
604                    let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH;
605
606                    // Copy RGB channels as is
607                    self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel);
608                    // Set alpha channel to opaque(0xFF)
609                    self.data[dst_idx + 3] = 0xFF;
610                })
611        });
612
613        let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?;
614
615        Ok(update_rectangle)
616    }
617
618    pub(crate) fn apply_rgb24(
619        &mut self,
620        rgb24: &[u8],
621        update_rectangle: &InclusiveRectangle,
622        flip: bool,
623    ) -> SessionResult<InclusiveRectangle> {
624        const SRC_COLOR_DEPTH: usize = 3;
625        let rectangle_width = usize::from(update_rectangle.width());
626        let lines = rgb24.chunks_exact(rectangle_width * SRC_COLOR_DEPTH);
627        if flip {
628            self.apply_rgb24_iter(lines.rev(), update_rectangle)
629        } else {
630            self.apply_rgb24_iter(lines, update_rectangle)
631        }
632    }
633
634    // FIXME: this assumes PixelFormat::RgbA32
635    pub(crate) fn apply_rgb32_bitmap(
636        &mut self,
637        rgb32: &[u8],
638        format: PixelFormat,
639        update_rectangle: &InclusiveRectangle,
640    ) -> SessionResult<InclusiveRectangle> {
641        const SRC_COLOR_DEPTH: usize = 4;
642        const DST_COLOR_DEPTH: usize = 4;
643
644        let image_width = usize::from(self.width);
645        let rectangle_width = usize::from(update_rectangle.width());
646        let top = usize::from(update_rectangle.top);
647        let left = usize::from(update_rectangle.left);
648
649        let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?;
650
651        if format == self.pixel_format {
652            rgb32
653                .chunks_exact(rectangle_width * SRC_COLOR_DEPTH)
654                .rev()
655                .enumerate()
656                .for_each(|(row_idx, row)| {
657                    row.chunks_exact(SRC_COLOR_DEPTH)
658                        .enumerate()
659                        .for_each(|(col_idx, src_pixel)| {
660                            let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH;
661
662                            self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel);
663                        })
664                });
665        } else {
666            rgb32
667                .chunks_exact(rectangle_width * SRC_COLOR_DEPTH)
668                .rev()
669                .enumerate()
670                .try_for_each(|(row_idx, row)| {
671                    row.chunks_exact(SRC_COLOR_DEPTH)
672                        .enumerate()
673                        .try_for_each(|(col_idx, src_pixel)| {
674                            let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH;
675
676                            let c = format
677                                .read_color(src_pixel)
678                                .map_err(|err| custom_err!("read color", err))?;
679                            self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(&[c.r, c.g, c.b, c.a]);
680
681                            Ok(())
682                        })?;
683
684                    Ok(())
685                })?;
686        }
687
688        let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?;
689
690        Ok(update_rectangle)
691    }
692}