use std::collections::HashMap;
use std::path::Path;
use anyhow::{Context, Result};
use image::RgbaImage;
use pixelcoords_core::geometry::Point;
use pixelcoords_core::locate;
use pixelcoords_core::session::{MonitorMatch, MonitorRecord, SelectionRecord, SessionFile};
use crate::capture::{self, CaptureProvider};
pub struct Region<'a> {
pub index: usize,
pub record: &'a SelectionRecord,
pub monitor: usize,
pub origin: Point,
pub crop: RgbaImage,
}
impl Region<'_> {
pub fn template(&self) -> locate::Template {
locate::Template::from_rgba(
self.crop.width() as usize,
self.crop.height() as usize,
self.crop.as_raw(),
)
}
}
pub fn load<'a>(
session: &'a SessionFile,
dir: &Path,
label: Option<&str>,
) -> Result<Vec<Region<'a>>> {
anyhow::ensure!(
!session.selections.is_empty(),
"the session has no selections"
);
let wanted = pixelcoords_core::session::select_by_label(session, label);
if wanted.is_empty() {
let labels = pixelcoords_core::session::distinct_labels(session.selections.iter());
anyhow::bail!(
"no selection is labeled {:?}; labels in this session: {labels:?}",
label.unwrap_or_default()
);
}
wanted
.into_iter()
.map(|(index, record)| {
let monitor = session
.monitors
.iter()
.find(|m| m.index == record.monitor)
.with_context(|| {
format!("the session does not describe monitor {}", record.monitor)
})?;
let crop_path = dir.join(&record.crop);
let crop = image::open(&crop_path)
.with_context(|| format!("reading crop {}", crop_path.display()))?
.to_rgba8();
Ok(Region {
index,
record,
monitor: record.monitor,
origin: locate::crop_origin(
&record.px,
record.rot_deg.unwrap_or(0),
monitor.size_px,
),
crop,
})
})
.collect()
}
pub fn capture_frames<P: CaptureProvider>(
provider: &P,
session: &SessionFile,
regions: &[Region],
) -> Result<HashMap<usize, RgbaImage>> {
#[cfg(target_os = "macos")]
if !crate::mac::has_screen_capture_access() && !crate::mac::request_screen_capture_access() {
anyhow::bail!(
"screen recording permission denied — run `pixelcoords doctor` \
for instructions"
)
}
let current = provider.monitors()?;
let live: Vec<MonitorRecord> = current.iter().map(record_from_monitor).collect();
let mut frames: HashMap<usize, RgbaImage> = HashMap::new();
for index in regions.iter().map(|r| r.monitor) {
if frames.contains_key(&index) {
continue;
}
let record = session
.monitors
.iter()
.find(|m| m.index == index)
.with_context(|| format!("the session does not describe monitor {index}"))?;
let monitor = live_monitor_for(record, ¤t, &live)?;
frames.insert(index, provider.capture(monitor)?);
}
Ok(frames)
}
pub fn record_from_monitor(monitor: &capture::MonitorInfo) -> MonitorRecord {
MonitorRecord {
index: monitor.index,
name: monitor.name.clone(),
primary: monitor.primary,
origin_px: monitor.origin_physical(),
size_px: monitor.size_physical(),
scale: monitor.scale,
}
}
fn live_monitor_for<'a>(
record: &MonitorRecord,
current: &'a [capture::MonitorInfo],
live: &[MonitorRecord],
) -> Result<&'a capture::MonitorInfo> {
match pixelcoords_core::session::match_monitor(record, live) {
MonitorMatch::Found(i) => Ok(¤t[i]),
MonitorMatch::Changed(i) => {
let now = current[i].size_physical();
anyhow::bail!(
"{} changed since the session ({}x{} scale {} now, {}x{} scale {} then) — \
relocation needs the same display setup",
record.name,
now.w,
now.h,
current[i].scale,
record.size_px.w,
record.size_px.h,
record.scale
)
}
MonitorMatch::Missing => {
let attached: Vec<&str> = current.iter().map(|m| m.name.as_str()).collect();
anyhow::bail!(
"the session's monitor {} ({}, {}x{} scale {}) is not attached — \
attached now: {attached:?}",
record.index,
record.name,
record.size_px.w,
record.size_px.h,
record.scale
)
}
}
}