glass/browser/session/
visual.rs1use super::*;
8
9impl BrowserSession {
10 pub async fn screenshot_png(&self) -> BrowserResult<Vec<u8>> {
15 let data = self.screenshot_base64().await?;
16 Ok(STANDARD.decode(data.as_bytes())?)
17 }
18
19 pub async fn screenshot_base64(&self) -> BrowserResult<String> {
21 self.policy.require(PolicyCapability::Screenshot)?;
22 self.cdp
23 .with_current_route(async { Ok(self.cdp.screenshot("png").await?) })
24 .await
25 }
26
27 pub async fn capture_visual(
29 &self,
30 options: &VisualCaptureOptions,
31 ) -> BrowserResult<VisualCapture> {
32 self.policy.require(PolicyCapability::Screenshot)?;
33 validate_visual_options(options)?;
34 self.cdp
35 .with_current_route(async {
36 let metrics = self.cdp.get_layout_metrics().await?;
37 let dpr = runtime_value(&self.cdp.evaluate("devicePixelRatio").await?)?
38 .as_f64()
39 .unwrap_or(1.0);
40 let (_, selected_frame_id) = self.route_identity().await?;
41 let selected_child_frame = {
42 let topology = self.topology.lock().await;
43 topology
44 .frames
45 .iter()
46 .find(|frame| frame.id == selected_frame_id)
47 .is_some_and(|frame| frame.parent_id.is_some())
48 };
49 if selected_child_frame {
50 return Err("exact visual capture of a selected child frame is not supported; select its page target or the main frame".into());
51 }
52 let mut clip = options.clip;
53 if options.full_page {
54 clip = Some(visual_rect(&metrics["cssContentSize"])?);
55 } else if let Some(target) = options.target.as_deref() {
56 let element = self.resolve_element(target).await?;
57 let model = match (element.node_id, element.backend_dom_node_id) {
58 (Some(node_id), _) => self.cdp.get_box_model(node_id).await?,
59 (_, Some(backend_id)) => {
60 self.cdp.get_box_model_for_backend(backend_id).await?
61 }
62 _ => return Err("visual target has no DOM node identity".into()),
63 };
64 let mut element_clip = visual_quad_rect(&model["model"]["border"])?;
65 let viewport = visual_viewport_rect(&metrics["cssVisualViewport"])?;
66 element_clip.x += viewport.x;
67 element_clip.y += viewport.y;
68 clip = Some(element_clip);
69 } else if clip.is_none() && options.scale != 1.0 {
70 clip = Some(visual_viewport_rect(&metrics["cssVisualViewport"])?);
71 }
72 let viewport = visual_viewport_rect(&metrics["cssVisualViewport"])?;
73 validate_effective_visual_clip(
74 Some(clip.unwrap_or(viewport)),
75 if clip.is_some() { options.scale } else { dpr },
76 )?;
77 let mut params = serde_json::json!({
78 "format": options.format.as_cdp(),
79 "optimizeForSpeed": true,
80 "captureBeyondViewport": options.full_page || clip.is_some(),
81 "fromSurface": true
82 });
83 if let Some(quality) = options.quality {
84 params["quality"] = Value::from(quality);
85 }
86 if let Some(clip) = clip {
87 params["clip"] = serde_json::json!({
88 "x": clip.x,
89 "y": clip.y,
90 "width": clip.width,
91 "height": clip.height,
92 "scale": options.scale
93 });
94 }
95 if options.full_page {
96 let latest = self.cdp.get_layout_metrics().await?;
97 let latest_clip = visual_rect(&latest["cssContentSize"])?;
98 if !visual_clips_match(clip.expect("full-page capture has a clip"), latest_clip) {
99 return Err("full-page geometry changed during capture preparation".into());
100 }
101 } else if let Some(target) = options.target.as_deref() {
102 let element = self.resolve_element(target).await?;
103 let model = match (element.node_id, element.backend_dom_node_id) {
104 (Some(node_id), _) => self.cdp.get_box_model(node_id).await?,
105 (_, Some(backend_id)) => self.cdp.get_box_model_for_backend(backend_id).await?,
106 _ => return Err("visual target has no DOM node identity".into()),
107 };
108 let latest_metrics = self.cdp.get_layout_metrics().await?;
109 let viewport = visual_viewport_rect(&latest_metrics["cssVisualViewport"])?;
110 let mut latest_clip = visual_quad_rect(&model["model"]["border"])?;
111 latest_clip.x += viewport.x;
112 latest_clip.y += viewport.y;
113 if !visual_clips_match(clip.expect("element capture has a clip"), latest_clip) {
114 return Err("element geometry changed during capture preparation".into());
115 }
116 }
117 let data = self.cdp.screenshot_with_params(params).await?;
118 if data.len() > MAX_VISUAL_BASE64_BYTES {
119 return Err("visual base64 payload exceeded 64 MiB".into());
120 }
121 let encoded_bytes = decoded_base64_len(&data)?;
122 let header_end = data.len().min(VISUAL_HEADER_BASE64_BYTES) / 4 * 4;
123 let header = STANDARD.decode(&data.as_bytes()[..header_end])?;
124 let size = imagesize::blob_size(&header)?;
125 let (target_id, frame_id) = self.route_identity().await?;
126 Ok(VisualCapture {
127 metadata: VisualCaptureMetadata {
128 format: options.format,
129 width: size.width,
130 height: size.height,
131 encoded_bytes,
132 device_scale_factor: dpr,
133 scale: options.scale,
134 full_page: options.full_page,
135 clip,
136 target_id,
137 frame_id,
138 },
139 data,
140 })
141 })
142 .await
143 }
144
145 pub async fn start_screencast(
146 &self,
147 format: VisualFormat,
148 quality: u8,
149 max_width: u32,
150 max_height: u32,
151 ) -> BrowserResult<ScreencastScope> {
152 self.policy.require(PolicyCapability::Screenshot)?;
153 if format == VisualFormat::Webp {
154 return Err("CDP screencast supports only png or jpeg".into());
155 }
156 if quality > 100
157 || max_width == 0
158 || max_height == 0
159 || max_width > 4096
160 || max_height > 4096
161 || f64::from(max_width) * f64::from(max_height) > MAX_VISUAL_PIXELS
162 {
163 return Err(
164 "screencast quality must be 0..=100 and dimensions must fit the 8 MP budget".into(),
165 );
166 }
167 let session_id = self.cdp.current_session_id();
168 let receiver = self.cdp.open_screencast_channel(session_id.clone())?;
169 let mut startup = ScreencastStartupGuard {
170 cdp: self.cdp.clone(),
171 session_id: session_id.clone(),
172 armed: true,
173 };
174 let parameters = Some(serde_json::json!({
175 "format": format.as_cdp(),
176 "quality": quality,
177 "maxWidth": max_width,
178 "maxHeight": max_height,
179 "everyNthFrame": 1
180 }));
181 let start_result = match session_id.as_deref() {
182 Some(session_id) => {
183 self.cdp
184 .send_to_session(session_id, "Page.startScreencast", parameters)
185 .await
186 }
187 None => self.cdp.send("Page.startScreencast", parameters).await,
188 };
189 if let Err(error) = start_result {
190 return Err(error.into());
191 }
192 startup.disarm();
193 Ok(ScreencastScope {
194 cdp: self.cdp.clone(),
195 session_id,
196 receiver,
197 armed: true,
198 })
199 }
200}