Skip to main content

crabshell_render/
capture.rs

1//! Captured frames: a compositor's buffer turned into the frame a
2//! screencopy view keeps, scaled down so a small view of a large output
3//! keeps a small frame.
4
5use std::rc::Rc;
6
7use crabshell_ui::{CaptureFrame, Fit};
8use tiny_skia::PixmapRef;
9
10use crate::PixelFormat;
11use crate::icons::halve;
12use crate::images;
13
14/// How the compositor turned a captured buffer, as `wl_output.transform`
15/// says: rotated counter-clockwise by a quarter turn or more, and flipped
16/// around the vertical axis before that.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct Turn {
19  /// Quarter turns counter-clockwise, 0 to 3.
20  pub quarters: u8,
21  /// Flipped around the vertical axis first.
22  pub flipped: bool,
23}
24
25impl Turn {
26  /// Whether the buffer's sides are swapped against what shows.
27  fn sideways(self) -> bool {
28    self.quarters % 2 == 1
29  }
30}
31
32/// The frame a view keeps of a `width` by `height` buffer of premultiplied
33/// pixels in `format`, which the compositor `turn`ed. With `opaque`, as in
34/// `XRGB8888` and `XBGR8888`, the fourth byte is ignored. The frame is
35/// turned back upright and scaled down to [`frame_size`] for `cover`.
36/// `None` if `pixels` doesn't hold `width * height` pixels.
37pub fn capture_frame(
38  pixels: &[u8],
39  width: u32,
40  height: u32,
41  format: PixelFormat,
42  opaque: bool,
43  turn: Turn,
44  cover: (u32, u32),
45) -> Option<CaptureFrame> {
46  let source = PixmapRef::from_bytes(pixels, width, height)?;
47  // Scaled while still turned, so the box is turned to match.
48  let cover = if turn.sideways() {
49    (cover.1, cover.0)
50  } else {
51    cover
52  };
53  let (kept_width, kept_height) = frame_size((width, height), cover);
54  // Halving averages, so the fourth byte can be filled in after it, in
55  // a quarter of the pixels; bicubic scaling would clamp to it first.
56  let mut first = if kept_width * 2 <= width && kept_height * 2 <= height {
57    halve(source).ok()?
58  } else {
59    source.to_owned()
60  };
61  if opaque {
62    for pixel in first.data_mut().as_chunks_mut::<4>().0 {
63      pixel[3] = 255;
64    }
65  }
66  // Scaling treats the four channels alike, so the pixels are swapped
67  // into RGBA only once they are few.
68  let mut kept = if (first.width(), first.height()) == (kept_width, kept_height) {
69    first
70  } else {
71    images::scale(first.as_ref(), kept_width, kept_height, Fit::Fill).ok()?
72  };
73  if format == PixelFormat::Argb8888 {
74    for pixel in kept.data_mut().as_chunks_mut::<4>().0 {
75      pixel.swap(0, 2);
76    }
77  }
78  let (width, height, rgba) = upright(kept_width, kept_height, kept.take(), turn);
79  Some(CaptureFrame {
80    width,
81    height,
82    rgba: Rc::from(rgba),
83  })
84}
85
86/// A `width` by `height` image the compositor `turn`ed, turned back.
87fn upright(width: u32, height: u32, rgba: Vec<u8>, turn: Turn) -> (u32, u32, Vec<u8>) {
88  if turn == Turn::default() {
89    return (width, height, rgba);
90  }
91  let (w, h) = (width as usize, height as usize);
92  let (out_w, out_h) = if turn.sideways() { (h, w) } else { (w, h) };
93  let mut out = vec![0; rgba.len()];
94  for y in 0..out_h {
95    for x in 0..out_w {
96      // Where the shown pixel is in the buffer: undoing the turn is the
97      // turn's inverse, then the flip, as the protocol applies the flip
98      // first.
99      let (bx, by) = match turn.quarters % 4 {
100        0 => (x, y),
101        1 => (y, h - 1 - x),
102        2 => (w - 1 - x, h - 1 - y),
103        _ => (w - 1 - y, x),
104      };
105      let bx = if turn.flipped { w - 1 - bx } else { bx };
106      let (from, to) = ((by * w + bx) * 4, (y * out_w + x) * 4);
107      out[to..to + 4].copy_from_slice(&rgba[from..from + 4]);
108    }
109  }
110  (out_w as u32, out_h as u32, out)
111}
112
113/// The size a `source` sized frame is kept at to show in a box of
114/// `cover` pixels: scaled down, keeping its shape, to the smallest size
115/// that still covers the box, so any fit draws it without scaling up. It
116/// is never scaled up, and keeps at least one pixel a side.
117pub fn frame_size(source: (u32, u32), cover: (u32, u32)) -> (u32, u32) {
118  let across = f64::from(cover.0) / f64::from(source.0.max(1));
119  let down = f64::from(cover.1) / f64::from(source.1.max(1));
120  let factor = across.max(down).min(1.0);
121  // `as` saturates, and the factor is at most 1, so this stays in range.
122  let side = |length: u32| ((f64::from(length) * factor).round() as u32).max(1);
123  (side(source.0), side(source.1))
124}