151 lines
4.5 KiB
Rust
151 lines
4.5 KiB
Rust
use crate::recording::Crop;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) struct Region {
|
|
pub(crate) x: i32,
|
|
pub(crate) y: i32,
|
|
pub(crate) width: i32,
|
|
pub(crate) height: i32,
|
|
}
|
|
|
|
impl Region {
|
|
pub(crate) fn parse(value: &str) -> Result<Self, String> {
|
|
let (position, size) = value
|
|
.trim()
|
|
.split_once(' ')
|
|
.ok_or("Invalid region selection")?;
|
|
let (x, y) = position.split_once(',').ok_or("Invalid region position")?;
|
|
let (width, height) = size.split_once('x').ok_or("Invalid region size")?;
|
|
let number = |value: &str| {
|
|
value
|
|
.parse::<i32>()
|
|
.map_err(|_| "Invalid region coordinate".to_owned())
|
|
};
|
|
let region = Self {
|
|
x: number(x)?,
|
|
y: number(y)?,
|
|
width: number(width)?,
|
|
height: number(height)?,
|
|
};
|
|
|
|
if region.width <= 0
|
|
|| region.height <= 0
|
|
|| region.x.checked_add(region.width).is_none()
|
|
|| region.y.checked_add(region.height).is_none()
|
|
{
|
|
return Err("Invalid region bounds".into());
|
|
}
|
|
|
|
Ok(region)
|
|
}
|
|
|
|
pub(crate) fn crop(self, output: Self, physical: (u32, u32)) -> Result<Crop, String> {
|
|
let x = i64::from(self.x) - i64::from(output.x);
|
|
let y = i64::from(self.y) - i64::from(output.y);
|
|
let end_x = x + i64::from(self.width);
|
|
let end_y = y + i64::from(self.height);
|
|
|
|
if self.width <= 0
|
|
|| self.height <= 0
|
|
|| output.width <= 0
|
|
|| output.height <= 0
|
|
|| x < 0
|
|
|| y < 0
|
|
|| end_x > i64::from(output.width)
|
|
|| end_y > i64::from(output.height)
|
|
|| physical.0 == 0
|
|
|| physical.1 == 0
|
|
|| physical.0 > i32::MAX as u32
|
|
|| physical.1 > i32::MAX as u32
|
|
{
|
|
return Err("Select a region entirely inside the chosen display".into());
|
|
}
|
|
// Round inward so pixels outside the selected logical rectangle never
|
|
// enter the recording, including at fractional output scales.
|
|
let near = |value: i64, pixels: u32, logical: i32| {
|
|
(value * i64::from(pixels) + i64::from(logical) - 1) / i64::from(logical)
|
|
};
|
|
let far =
|
|
|value: i64, pixels: u32, logical: i32| value * i64::from(pixels) / i64::from(logical);
|
|
let left = near(x, physical.0, output.width);
|
|
let top = near(y, physical.1, output.height);
|
|
let end_x = far(end_x, physical.0, output.width);
|
|
let end_y = far(end_y, physical.1, output.height);
|
|
|
|
if end_x <= left || end_y <= top {
|
|
return Err("The selected region is smaller than one output pixel".into());
|
|
}
|
|
|
|
Ok(Crop {
|
|
left: left as i32,
|
|
top: top as i32,
|
|
right: (i64::from(physical.0) - end_x) as i32,
|
|
bottom: (i64::from(physical.1) - end_y) as i32,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn region_mapping_handles_negative_origins_and_fractional_scales() {
|
|
let output = Region::parse("-1920,20 1920x1080").unwrap();
|
|
let selected = Region::parse("-1919,21 100x50").unwrap();
|
|
assert_eq!(
|
|
selected.crop(output, (2400, 1350)).unwrap(),
|
|
Crop {
|
|
left: 2,
|
|
top: 2,
|
|
right: 2274,
|
|
bottom: 1287
|
|
}
|
|
);
|
|
assert_eq!(
|
|
output.crop(output, (2880, 1620)).unwrap(),
|
|
Crop {
|
|
left: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
}
|
|
);
|
|
assert!(
|
|
Region::parse("-1921,20 100x50")
|
|
.unwrap()
|
|
.crop(output, (2400, 1350))
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
Region::parse("-100,20 200x50")
|
|
.unwrap()
|
|
.crop(output, (2400, 1350))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn region_selection_rejects_empty_overflow_and_malformed_bounds() {
|
|
for value in [
|
|
"",
|
|
"1,2 0x20",
|
|
"1,2 -1x20",
|
|
"2147483647,0 2x20",
|
|
"1,2 20x30 extra",
|
|
] {
|
|
assert!(Region::parse(value).is_err(), "{value}");
|
|
}
|
|
assert!(
|
|
Crop {
|
|
left: -1,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
}
|
|
.validate()
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|