Separate fullscreen capture from core.rs
- Modularity and abstraction - Separation of concerns
This commit is contained in:
parent
39ef220c98
commit
60fe3bf12c
93
src/core.rs
93
src/core.rs
@ -5,28 +5,38 @@ use x11rb::rust_connection::RustConnection;
|
|||||||
use png::{BitDepth, ColorType, Encoder};
|
use png::{BitDepth, ColorType, Encoder};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
/// Holds an open X11 connection and basic screen geometry.
|
/// Stores:
|
||||||
/// All screenshot binaries share this context.
|
/// - a connection to the X11 server (`conn`)
|
||||||
|
/// - the root window (the whole screen)
|
||||||
|
/// - the screen width and height in pixels
|
||||||
|
///
|
||||||
|
/// Every screenshot function uses this.
|
||||||
pub struct XContext {
|
pub struct XContext {
|
||||||
pub conn: RustConnection,
|
pub conn: RustConnection,
|
||||||
pub root: u32, // The root window of the screen (represents the whole desktop)
|
pub root: u32,
|
||||||
pub width: u16, // Screen width in pixels
|
pub width: u16,
|
||||||
pub height: u16, // Screen height in pixels
|
pub height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XContext {
|
impl XContext {
|
||||||
|
/// Create a new connection to the X11 server and read basic screen info.
|
||||||
|
///
|
||||||
|
/// This does not take a screenshot. It only connects and stores
|
||||||
|
/// values we need later.
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
// Establish a connection to the running X server.
|
// Connect to the X11 server running on the system.
|
||||||
// `screen_num` selects which screen we are connected to (multi-head setups).
|
|
||||||
let (conn, screen_num) = RustConnection::connect(None)?;
|
let (conn, screen_num) = RustConnection::connect(None)?;
|
||||||
|
|
||||||
// Extract the geometry information from the X11 setup.
|
// Read screen information from the X11 setup data.
|
||||||
// These fields are Copy, so borrowing ends immediately.
|
// We copy only the simple numeric values we need.
|
||||||
let screen = &conn.setup().roots[screen_num];
|
let setup = conn.setup();
|
||||||
|
let screen = &setup.roots[screen_num];
|
||||||
|
|
||||||
let root = screen.root;
|
let root = screen.root;
|
||||||
let width = screen.width_in_pixels;
|
let width = screen.width_in_pixels;
|
||||||
let height = screen.height_in_pixels;
|
let height = screen.height_in_pixels;
|
||||||
|
|
||||||
|
// Return our context object.
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
conn,
|
conn,
|
||||||
root,
|
root,
|
||||||
@ -36,66 +46,67 @@ impl XContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Capture the *entire* root window (full screen) into an RGBA8 buffer.
|
/// Capture a rectangular part of the screen.
|
||||||
///
|
///
|
||||||
/// X11 provides the `GetImage` request, which returns raw pixel data from a drawable.
|
/// `ctx` – connection + screen info
|
||||||
/// We request the `ZPixmap` format, which returns packed pixel values.
|
/// `x,y` – top-left corner of the rectangle
|
||||||
|
/// `w,h` – size of the rectangle
|
||||||
///
|
///
|
||||||
/// Many x11rb code generators include a named constant for `ZPixmap`.
|
/// Returns: a `Vec<u8>` containing RGBA pixel data.
|
||||||
/// If not available, its protocol value is simply `2`.
|
pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::Result<Vec<u8>> {
|
||||||
pub fn capture_root_image(ctx: &XContext) -> anyhow::Result<Vec<u8>> {
|
// X11 uses the number 2 for "ZPixmap", which means: return full pixel data.
|
||||||
// ZPixmap corresponds to the X11 format that returns full pixel values.
|
|
||||||
// Using a literal here avoids dependency on generated constant names.
|
|
||||||
let format_zpixmap: u8 = 2;
|
let format_zpixmap: u8 = 2;
|
||||||
|
|
||||||
// Send the GetImage request for the entire screen.
|
// Ask X11 to send us the raw pixels in the requested rectangle.
|
||||||
|
// This returns X11's native pixel format (usually B,G,R,unused).
|
||||||
let reply = ctx
|
let reply = ctx
|
||||||
.conn
|
.conn
|
||||||
.get_image(
|
.get_image(
|
||||||
format_zpixmap.into(), // ZPixmap format
|
format_zpixmap.into(),
|
||||||
ctx.root, // The root window (whole desktop)
|
ctx.root,
|
||||||
0, // X origin
|
x as i16,
|
||||||
0, // Y origin
|
y as i16,
|
||||||
ctx.width, // Width to capture
|
w,
|
||||||
ctx.height, // Height to capture
|
h,
|
||||||
u32::MAX, // Plane mask: capture all pixel planes
|
u32::MAX,
|
||||||
)?
|
)?
|
||||||
.reply()?;
|
.reply()?;
|
||||||
|
|
||||||
let data = reply.data;
|
let data = reply.data;
|
||||||
let depth = reply.depth;
|
|
||||||
|
|
||||||
// Modern X11 desktops typically use depth 24 (RGB) or 32 (RGB with padding).
|
// Prepare a buffer for RGBA output.
|
||||||
if depth != 24 && depth != 32 {
|
// Each pixel = 4 bytes (R, G, B, A).
|
||||||
anyhow::bail!("Unsupported X11 depth: {depth}");
|
let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
|
||||||
}
|
|
||||||
|
|
||||||
// X11 typically stores pixels in little-endian B,G,R,(unused) format.
|
|
||||||
// We convert it to standard RGBA because PNG expects that.
|
|
||||||
let mut out = Vec::with_capacity((ctx.width as usize) * (ctx.height as usize) * 4);
|
|
||||||
|
|
||||||
|
// Convert each incoming pixel from X11's format (B, G, R, X)
|
||||||
|
// into the common RGBA format that PNG expects.
|
||||||
for chunk in data.chunks_exact(4) {
|
for chunk in data.chunks_exact(4) {
|
||||||
let b = chunk[0];
|
let b = chunk[0];
|
||||||
let g = chunk[1];
|
let g = chunk[1];
|
||||||
let r = chunk[2];
|
let r = chunk[2];
|
||||||
out.extend_from_slice(&[r, g, b, 0xFF]); // Always opaque
|
|
||||||
|
// A (alpha) is set to 255 = fully opaque.
|
||||||
|
out.extend_from_slice(&[r, g, b, 0xFF]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save a raw RGBA8 buffer as a PNG image at `path`.
|
/// Save an RGBA pixel buffer as a PNG file.
|
||||||
///
|
///
|
||||||
/// The png crate handles all compression and file formatting.
|
/// `path` – where to write the file
|
||||||
/// We simply specify:
|
/// `width` – image width in pixels
|
||||||
/// - RGBA layout
|
/// `height` – image height in pixels
|
||||||
/// - 8 bits per channel
|
/// `rgba` – pixel data in RGBA format
|
||||||
pub fn save_png(path: &str, width: u16, height: u16, rgba: &[u8]) -> anyhow::Result<()> {
|
pub fn save_png(path: &str, width: u16, height: u16, rgba: &[u8]) -> anyhow::Result<()> {
|
||||||
let file = File::create(path)?;
|
let file = File::create(path)?;
|
||||||
|
|
||||||
|
// Create a PNG encoder for an RGBA 8-bit image.
|
||||||
let mut encoder = Encoder::new(file, width.into(), height.into());
|
let mut encoder = Encoder::new(file, width.into(), height.into());
|
||||||
encoder.set_color(ColorType::Rgba);
|
encoder.set_color(ColorType::Rgba);
|
||||||
encoder.set_depth(BitDepth::Eight);
|
encoder.set_depth(BitDepth::Eight);
|
||||||
|
|
||||||
|
// Write PNG header + pixel data.
|
||||||
let mut writer = encoder.write_header()?;
|
let mut writer = encoder.write_header()?;
|
||||||
writer.write_image_data(rgba)?;
|
writer.write_image_data(rgba)?;
|
||||||
writer.finish()?;
|
writer.finish()?;
|
||||||
|
|||||||
@ -1,38 +1,42 @@
|
|||||||
mod core;
|
mod core;
|
||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use core::{XContext, capture_root_image, save_png};
|
use core::{XContext, capture_rect, save_png};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
// Establish connection to X11 and gather screen info.
|
// Create a connection to the X11 server and read screen information.
|
||||||
let ctx = XContext::new()?;
|
let ctx = XContext::new()?;
|
||||||
|
|
||||||
// Capture the entire screen as an RGBA8 buffer.
|
// Capture the entire screen.
|
||||||
let img = capture_root_image(&ctx)?;
|
// We pass (0,0) as the top-left corner and use the screen width/height.
|
||||||
|
let img = capture_rect(&ctx, 0, 0, ctx.width, ctx.height)?;
|
||||||
|
|
||||||
// Build an output path:
|
// Build the output file path.
|
||||||
// ~/Pictures/Screenshots/screenshot_YYYYMMDDHHMMSS.png
|
// Example:
|
||||||
|
// ~/Pictures/Screenshots/screenshot_20250116094530.png
|
||||||
//
|
//
|
||||||
// Using timestamps avoids overwriting previous screenshots,
|
// Using a timestamp prevents overwriting old screenshots.
|
||||||
// and matches the behavior of many modern screenshot tools.
|
|
||||||
let timestamp = Local::now().format("%Y%m%d%H%M%S").to_string();
|
let timestamp = Local::now().format("%Y%m%d%H%M%S").to_string();
|
||||||
|
|
||||||
|
// Start from the user's home directory (fallback: current dir).
|
||||||
let mut path = dirs::home_dir().unwrap_or_else(|| ".".into());
|
let mut path = dirs::home_dir().unwrap_or_else(|| ".".into());
|
||||||
path.push("Pictures");
|
path.push("Pictures");
|
||||||
path.push("Screenshots");
|
path.push("Screenshots");
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Make sure the directory exists.
|
||||||
fs::create_dir_all(&path)?;
|
fs::create_dir_all(&path)?;
|
||||||
|
|
||||||
|
// Add the actual filename.
|
||||||
path.push(format!("screenshot_{timestamp}.png"));
|
path.push(format!("screenshot_{timestamp}.png"));
|
||||||
|
|
||||||
// Convert PathBuf → &str (safe because it’s UTF-8 on all Linux systems).
|
// Convert path to string for save_png().
|
||||||
let path_str = path.to_string_lossy();
|
let path_str = path.to_string_lossy();
|
||||||
|
|
||||||
// Write the PNG file to disk.
|
// Write the captured RGBA image to a PNG file.
|
||||||
save_png(&path_str, ctx.width, ctx.height, &img)?;
|
save_png(&path_str, ctx.width, ctx.height, &img)?;
|
||||||
|
|
||||||
println!("Saved {path_str}");
|
println!("Saved {path_str}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user