use std::path::Path;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as Base64Standard;
use tracing::debug;
use crate::error::{Error, Result};
use crate::protocol::command::{BrowsingContextCommand, Command};
use super::Tab;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ImageFormat {
#[default]
Png,
Jpeg(u8),
}
impl ImageFormat {
#[inline]
#[must_use]
pub fn png() -> Self {
Self::Png
}
#[inline]
#[must_use]
pub fn jpeg(quality: u8) -> Self {
Self::Jpeg(quality.min(100))
}
#[must_use]
pub fn mime_type(&self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpeg(_) => "image/jpeg",
}
}
#[must_use]
pub fn extension(&self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg(_) => "jpg",
}
}
fn format_str(&self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg(_) => "jpeg",
}
}
fn quality(&self) -> Option<u8> {
match self {
Self::Png => None,
Self::Jpeg(q) => Some(*q),
}
}
}
pub struct ScreenshotBuilder<'a> {
tab: &'a Tab,
format: ImageFormat,
}
impl<'a> ScreenshotBuilder<'a> {
pub(crate) fn new(tab: &'a Tab) -> Self {
Self {
tab,
format: ImageFormat::Png,
}
}
#[must_use]
pub fn png(mut self) -> Self {
self.format = ImageFormat::Png;
self
}
#[must_use]
pub fn jpeg(mut self, quality: u8) -> Self {
self.format = ImageFormat::Jpeg(quality.min(100));
self
}
#[must_use]
pub fn format(mut self, format: ImageFormat) -> Self {
self.format = format;
self
}
pub async fn capture(&self) -> Result<String> {
debug!(
tab_id = %self.tab.inner.tab_id,
format = ?self.format,
"Capturing screenshot via browser API"
);
let command = Command::BrowsingContext(BrowsingContextCommand::CaptureScreenshot {
format: self.format.format_str().to_string(),
quality: self.format.quality(),
});
let response = self.tab.send_command(command).await?;
debug!(response = ?response, "Screenshot response");
let data = response
.result
.as_ref()
.and_then(|v| v.get("data"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
let result_str = response
.result
.as_ref()
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string());
Error::script_error(format!(
"Screenshot response missing data field. Got: {}",
result_str
))
})?;
Ok(data.to_string())
}
pub async fn capture_bytes(&self) -> Result<Vec<u8>> {
let base64_data = self.capture().await?;
Base64Standard
.decode(&base64_data)
.map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))
}
pub async fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let bytes = self.capture_bytes().await?;
tokio::fs::write(path.as_ref(), bytes).await.map_err(Error::Io)?;
Ok(())
}
}
impl Tab {
#[must_use]
pub fn screenshot(&self) -> ScreenshotBuilder<'_> {
ScreenshotBuilder::new(self)
}
pub async fn capture_screenshot(&self) -> Result<String> {
self.screenshot().png().capture().await
}
pub async fn save_screenshot(&self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("png")
.to_lowercase();
let builder = self.screenshot();
let builder = match ext.as_str() {
"jpg" | "jpeg" => builder.jpeg(85),
_ => builder.png(),
};
builder.save(path).await
}
}