1use crate::wl::{CapturedImage, Client, Frame, Output, Region};
11use anyhow::{Context, Result, bail};
12use std::time::Duration;
13
14pub const DEFAULT_BUDGET: Duration = Duration::from_secs(2);
16
17pub fn frame_to_image(frame: Frame) -> Result<CapturedImage> {
22 match frame {
23 Frame::Shm(img) => Ok(img),
24 Frame::Dmabuf(d) => crate::gl::GpuReadback::new()
25 .and_then(|mut rb| rb.readback(d))
26 .context("readback GPU de la frame dma-buf"),
27 }
28}
29
30pub fn capture_output(
32 client: &mut Client,
33 name: Option<&str>,
34 budget: Duration,
35) -> Result<CapturedImage> {
36 let outputs = client.outputs().to_vec();
37 let output = match name {
38 Some(n) => outputs
39 .iter()
40 .find(|o| o.name == n)
41 .with_context(|| format!("output '{n}' not found"))?,
42 None => match outputs.as_slice() {
43 [single] => single,
44 [] => bail!("no outputs available"),
45 many => {
46 let names: Vec<&str> = many.iter().map(|o| o.name.as_str()).collect();
47 bail!(
48 "multiple outputs; specify -o NAME among: {}",
49 names.join(", ")
50 );
51 }
52 },
53 };
54 frame_to_image(client.capture_output_once(output, budget)?)
55}
56
57pub fn capture_window(client: &mut Client, id: &str, budget: Duration) -> Result<CapturedImage> {
59 let tl = client
60 .toplevels()
61 .iter()
62 .find(|t| t.identifier == id)
63 .cloned()
64 .with_context(|| format!("window '{id}' not found"))?;
65 frame_to_image(client.capture_toplevel_once(&tl, budget)?)
66}
67
68pub struct OutputCapture {
70 pub output: Output,
72 pub image: CapturedImage,
74}
75
76pub fn capture_all(client: &mut Client, budget: Duration) -> Result<Vec<OutputCapture>> {
79 let outputs = client.outputs().to_vec();
80 if outputs.is_empty() {
81 bail!("no outputs available");
82 }
83 let mut caps = Vec::with_capacity(outputs.len());
84 for output in outputs {
85 let image = frame_to_image(client.capture_output_once(&output, budget)?)?;
86 caps.push(OutputCapture { output, image });
87 }
88 Ok(caps)
89}
90
91pub fn composite(caps: &[OutputCapture], region: Region) -> Result<CapturedImage> {
95 if region.is_empty() {
96 bail!("empty region");
97 }
98 let covering: Vec<&OutputCapture> = caps
99 .iter()
100 .filter(|c| region.intersect(&c.output.logical_rect()).is_some())
101 .collect();
102
103 match covering.as_slice() {
104 [] => bail!("region covers no output"),
105 [c] => {
107 let inter = region.intersect(&c.output.logical_rect()).unwrap();
108 Ok(c.image.crop(logical_to_physical(&c.output, inter)))
109 }
110 many => {
112 let (dw, dh) = (region.w, region.h);
113 let mut dst = vec![0u8; (dw as usize) * (dh as usize) * 4];
114 for c in many {
115 let inter = region.intersect(&c.output.logical_rect()).unwrap();
116 let phys = logical_to_physical(&c.output, inter);
117 let logical = resize(c.image.crop(phys), inter.w, inter.h);
118 logical.blit_into(&mut dst, dw, dh, inter.x - region.x, inter.y - region.y);
119 }
120 Ok(CapturedImage {
121 width: dw,
122 height: dh,
123 rgba: dst,
124 })
125 }
126 }
127}
128
129pub fn capture_region(
132 client: &mut Client,
133 region: Region,
134 budget: Duration,
135) -> Result<CapturedImage> {
136 if region.is_empty() {
137 bail!("empty region");
138 }
139 let outputs: Vec<Output> = client
140 .outputs()
141 .iter()
142 .filter(|o| region.intersect(&o.logical_rect()).is_some())
143 .cloned()
144 .collect();
145 if outputs.is_empty() {
146 bail!("region covers no output");
147 }
148 let mut caps = Vec::with_capacity(outputs.len());
149 for output in outputs {
150 let image = frame_to_image(client.capture_output_once(&output, budget)?)?;
151 caps.push(OutputCapture { output, image });
152 }
153 composite(&caps, region)
154}
155
156pub fn whole_layout(client: &Client) -> Result<Region> {
158 let mut it = client.outputs().iter().map(Output::logical_rect);
159 let first = it.next().context("no outputs available")?;
160 let (mut x0, mut y0) = (first.x, first.y);
161 let (mut x1, mut y1) = (first.x + first.w as i32, first.y + first.h as i32);
162 for r in it {
163 x0 = x0.min(r.x);
164 y0 = y0.min(r.y);
165 x1 = x1.max(r.x + r.w as i32);
166 y1 = y1.max(r.y + r.h as i32);
167 }
168 Ok(Region {
169 x: x0,
170 y: y0,
171 w: (x1 - x0) as u32,
172 h: (y1 - y0) as u32,
173 })
174}
175
176pub fn logical_to_physical(output: &Output, logical: Region) -> Region {
179 let (lw, lh) = output.logical_size();
180 let sx = output.phys_width as f64 / lw.max(1) as f64;
181 let sy = output.phys_height as f64 / lh.max(1) as f64;
182 let lr = output.logical_rect();
183 Region {
184 x: (((logical.x - lr.x) as f64) * sx).round() as i32,
185 y: (((logical.y - lr.y) as f64) * sy).round() as i32,
186 w: ((logical.w as f64) * sx).round() as u32,
187 h: ((logical.h as f64) * sy).round() as u32,
188 }
189}
190
191pub fn resize(img: CapturedImage, nw: u32, nh: u32) -> CapturedImage {
193 if (img.width, img.height) == (nw, nh) || nw == 0 || nh == 0 {
194 return img;
195 }
196 let Some(buf) = image::RgbaImage::from_raw(img.width, img.height, img.rgba) else {
197 return CapturedImage {
198 width: 0,
199 height: 0,
200 rgba: Vec::new(),
201 };
202 };
203 let small = image::imageops::resize(&buf, nw, nh, image::imageops::FilterType::Triangle);
204 CapturedImage {
205 width: small.width(),
206 height: small.height(),
207 rgba: small.into_raw(),
208 }
209}
210
211pub fn encode_png(img: &CapturedImage) -> Result<Vec<u8>> {
214 let buf = image::RgbaImage::from_raw(img.width, img.height, img.rgba.clone())
215 .ok_or_else(|| anyhow::anyhow!("image dimensions don't match the buffer"))?;
216 let mut out = std::io::Cursor::new(Vec::new());
217 image::DynamicImage::ImageRgba8(buf)
218 .write_to(&mut out, image::ImageFormat::Png)
219 .context("PNG encode")?;
220 Ok(out.into_inner())
221}
222
223pub fn parse_geometry(s: &str) -> Result<Region> {
225 let err = || anyhow::anyhow!("invalid geometry '{s}' (expected 'X,Y WxH')");
226 let (pos, size) = s.trim().split_once(' ').ok_or_else(err)?;
227 let (x, y) = pos.split_once(',').ok_or_else(err)?;
228 let (w, h) = size.split_once(['x', 'X', '×']).ok_or_else(err)?;
229 Ok(Region {
230 x: x.trim().parse().map_err(|_| err())?,
231 y: y.trim().parse().map_err(|_| err())?,
232 w: w.trim().parse().map_err(|_| err())?,
233 h: h.trim().parse().map_err(|_| err())?,
234 })
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn parse_geometry_ok() {
243 let r = parse_geometry("10,20 300x400").unwrap();
244 assert_eq!((r.x, r.y, r.w, r.h), (10, 20, 300, 400));
245 let r = parse_geometry("-5,-6 7x8").unwrap();
246 assert_eq!((r.x, r.y, r.w, r.h), (-5, -6, 7, 8));
247 }
248
249 #[test]
250 fn parse_geometry_bad() {
251 assert!(parse_geometry("nonsense").is_err());
252 assert!(parse_geometry("1,2 3").is_err());
253 }
254}