use crate::color::Rgba;
use crate::frame::FrameView;
use crate::geometry::Px;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
impl Direction {
pub const ALL: [Direction; 4] = [
Direction::Left,
Direction::Right,
Direction::Up,
Direction::Down,
];
fn step(self) -> (i32, i32) {
match self {
Direction::Left => (-1, 0),
Direction::Right => (1, 0),
Direction::Up => (0, -1),
Direction::Down => (0, 1),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Tolerance(pub u32);
impl Tolerance {
pub const DEFAULT: Tolerance = Tolerance(30);
pub const STRICT: Tolerance = Tolerance(8);
pub const LOOSE: Tolerance = Tolerance(90);
}
impl Default for Tolerance {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum EdgeBias {
Inner,
#[default]
Midpoint,
Outer,
}
impl EdgeBias {
pub fn label(self) -> &'static str {
match self {
EdgeBias::Inner => "Inner",
EdgeBias::Midpoint => "Midpoint",
EdgeBias::Outer => "Outer",
}
}
pub fn cycle(self) -> Self {
match self {
EdgeBias::Inner => EdgeBias::Midpoint,
EdgeBias::Midpoint => EdgeBias::Outer,
EdgeBias::Outer => EdgeBias::Inner,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct EdgeCandidate {
pub direction: Direction,
pub distance: u32,
pub position: Px,
pub anchor_color: Rgba,
pub edge_color: Rgba,
pub strength: u32,
pub edge_phys: f64,
}
pub type EdgeQuad = [Option<EdgeCandidate>; 4];
pub fn detect_edges(
frame: &FrameView,
cursor: Px,
tolerance: Tolerance,
bias: EdgeBias,
) -> EdgeQuad {
let Some(anchor) = pixel_for_cursor(frame, cursor) else {
return [None, None, None, None];
};
[
scan(frame, cursor, Direction::Left, anchor, tolerance, bias),
scan(frame, cursor, Direction::Right, anchor, tolerance, bias),
scan(frame, cursor, Direction::Up, anchor, tolerance, bias),
scan(frame, cursor, Direction::Down, anchor, tolerance, bias),
]
}
fn pixel_for_cursor(frame: &FrameView, cursor: Px) -> Option<Rgba> {
if cursor.x < 0 || cursor.y < 0 {
return None;
}
frame.pixel(cursor.x as u32, cursor.y as u32)
}
fn scan(
frame: &FrameView,
cursor: Px,
dir: Direction,
anchor: Rgba,
tol: Tolerance,
bias: EdgeBias,
) -> Option<EdgeCandidate> {
let (dx, dy) = dir.step();
let sample = |k: i32| -> Option<Rgba> {
let x = cursor.x + dx * k;
let y = cursor.y + dy * k;
if x < 0 || y < 0 {
return None;
}
frame.pixel(x as u32, y as u32)
};
let (first_over, here, boundary) = localize_edge(sample, anchor, tol.0, bias)?;
let pos = Px {
x: cursor.x + dx * first_over,
y: cursor.y + dy * first_over,
};
let edge_phys = match dir {
Direction::Left => cursor.x as f64 - boundary,
Direction::Right => cursor.x as f64 + boundary,
Direction::Up => cursor.y as f64 - boundary,
Direction::Down => cursor.y as f64 + boundary,
};
Some(EdgeCandidate {
direction: dir,
distance: first_over as u32,
position: pos,
anchor_color: anchor,
edge_color: here,
strength: anchor.rgb_delta(here),
edge_phys,
})
}
const MAX_GRADIENT: i32 = 64;
const PLATEAU_EPS: u32 = 8;
fn localize_edge(
sample: impl Fn(i32) -> Option<Rgba>,
anchor: Rgba,
tol: u32,
bias: EdgeBias,
) -> Option<(i32, Rgba, f64)> {
let mut first_over = 1;
let first_over_color = loop {
let here = sample(first_over)?;
if anchor.rgb_delta(here) > tol {
break here;
}
first_over += 1;
};
let last_inside = first_over - 1;
let limit = first_over + MAX_GRADIENT;
let mut prev = first_over_color;
let mut i = first_over + 1;
let first_outside = loop {
if i > limit {
break first_over;
}
match sample(i) {
Some(p) => {
if prev.rgb_delta(p) <= PLATEAU_EPS {
break i - 1;
}
prev = p;
i += 1;
}
None => break i - 1,
}
};
let outside = sample(first_outside)?;
let boundary = localize_within(sample, anchor, outside, last_inside, first_outside, bias);
Some((first_over, first_over_color, boundary))
}
fn localize_within(
sample: impl Fn(i32) -> Option<Rgba>,
inside: Rgba,
outside: Rgba,
last_inside: i32,
first_outside: i32,
bias: EdgeBias,
) -> f64 {
match bias {
EdgeBias::Inner => last_inside as f64 + 0.5,
EdgeBias::Outer => first_outside as f64 - 0.5,
EdgeBias::Midpoint => {
let geometric_centre = (last_inside + first_outside) as f64 / 2.0;
if inside.rgb_delta(outside) <= PLATEAU_EPS {
return geometric_centre;
}
let axis = (
outside.r as i32 - inside.r as i32,
outside.g as i32 - inside.g as i32,
outside.b as i32 - inside.b as i32,
);
let den = axis.0 * axis.0 + axis.1 * axis.1 + axis.2 * axis.2;
let proj = |c: Rgba| -> f64 {
let d = (
c.r as i32 - inside.r as i32,
c.g as i32 - inside.g as i32,
c.b as i32 - inside.b as i32,
);
(d.0 * axis.0 + d.1 * axis.1 + d.2 * axis.2) as f64 / den as f64
};
let mut prev_k = 0;
let mut prev_p = 0.0;
for k in 1..=first_outside {
let Some(c) = sample(k) else { break };
let p = proj(c);
if prev_p <= 0.5 && p > 0.5 {
return prev_k as f64 + (0.5 - prev_p) / (p - prev_p);
}
prev_k = k;
prev_p = p;
}
geometric_centre
}
}
}
pub fn shrink_to_content(
frame: &FrameView,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
tolerance: Tolerance,
bias: EdgeBias,
) -> (i32, i32, i32, i32) {
round_bounds(shrink_to_content_frac(
frame, x0, y0, x1, y1, tolerance, bias,
))
}
#[allow(clippy::too_many_arguments)]
pub fn shrink_to_content_with_bg(
frame: &FrameView,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
bg: Px,
tolerance: Tolerance,
bias: EdgeBias,
) -> (i32, i32, i32, i32) {
round_bounds(shrink_to_content_with_bg_frac(
frame, x0, y0, x1, y1, bg, tolerance, bias,
))
}
fn round_bounds((l, t, r, b): (f64, f64, f64, f64)) -> (i32, i32, i32, i32) {
(
(l + 0.5).round() as i32,
(t + 0.5).round() as i32,
(r - 0.5).round() as i32,
(b - 0.5).round() as i32,
)
}
pub fn shrink_to_content_frac(
frame: &FrameView,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
tolerance: Tolerance,
bias: EdgeBias,
) -> (f64, f64, f64, f64) {
let bg_x = x0.min(x1).max(0).min(frame.width as i32 - 1);
let bg_y = y0.min(y1).max(0).min(frame.height as i32 - 1);
shrink_to_content_with_bg_frac(frame, x0, y0, x1, y1, Px::new(bg_x, bg_y), tolerance, bias)
}
#[allow(clippy::too_many_arguments)]
pub fn shrink_to_content_with_bg_frac(
frame: &FrameView,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
bg: Px,
tolerance: Tolerance,
bias: EdgeBias,
) -> (f64, f64, f64, f64) {
let fallback = (
x0 as f64 - 0.5,
y0 as f64 - 0.5,
x1 as f64 + 0.5,
y1 as f64 + 0.5,
);
let (rx0, rx1) = (x0.min(x1), x0.max(x1));
let (ry0, ry1) = (y0.min(y1), y0.max(y1));
let fw = frame.width as i32;
let fh = frame.height as i32;
let cx0 = rx0.max(0).min(fw - 1);
let cy0 = ry0.max(0).min(fh - 1);
let cx1 = rx1.max(0).min(fw - 1);
let cy1 = ry1.max(0).min(fh - 1);
if cx1 <= cx0 || cy1 <= cy0 {
return fallback;
}
let bx = bg.x.max(0).min(fw - 1);
let by = bg.y.max(0).min(fh - 1);
let bg = match frame.pixel(bx as u32, by as u32) {
Some(p) => p,
None => return fallback,
};
let tol = tolerance.0;
let row_has_content = |y: i32, x_start: i32, x_end: i32| -> bool {
for x in x_start..=x_end {
if let Some(p) = frame.pixel(x as u32, y as u32) {
if bg.rgb_delta(p) > tol {
return true;
}
}
}
false
};
let col_has_content = |x: i32, y_start: i32, y_end: i32| -> bool {
for y in y_start..=y_end {
if let Some(p) = frame.pixel(x as u32, y as u32) {
if bg.rgb_delta(p) > tol {
return true;
}
}
}
false
};
let mut new_top = cy0;
for y in cy0..=cy1 {
if row_has_content(y, cx0, cx1) {
new_top = y;
break;
}
}
let mut new_bot = cy1;
for y in (new_top..=cy1).rev() {
if row_has_content(y, cx0, cx1) {
new_bot = y;
break;
}
}
let mut new_left = cx0;
for x in cx0..=cx1 {
if col_has_content(x, new_top, new_bot) {
new_left = x;
break;
}
}
let mut new_right = cx1;
for x in (new_left..=cx1).rev() {
if col_has_content(x, new_top, new_bot) {
new_right = x;
break;
}
}
if new_right <= new_left || new_bot <= new_top {
return fallback;
}
let mid_x = (new_left + new_right) / 2;
let mid_y = (new_top + new_bot) / 2;
let on_bg = |p: Option<Rgba>| matches!(p, Some(p) if bg.rgb_delta(p) <= tol);
let probe_h = |start_x: i32, at_y: i32, step: i32, fallback: f64| -> f64 {
let sample = |k: i32| -> Option<Rgba> {
let x = start_x + step * k;
if x < 0 || at_y < 0 {
return None;
}
frame.pixel(x as u32, at_y as u32)
};
if !on_bg(sample(0)) {
return fallback;
}
match localize_edge(sample, bg, tol, bias) {
Some((_, _, boundary)) => start_x as f64 + step as f64 * boundary,
None => fallback,
}
};
let probe_v = |at_x: i32, start_y: i32, step: i32, fallback: f64| -> f64 {
let sample = |k: i32| -> Option<Rgba> {
let y = start_y + step * k;
if y < 0 || at_x < 0 {
return None;
}
frame.pixel(at_x as u32, y as u32)
};
if !on_bg(sample(0)) {
return fallback;
}
match localize_edge(sample, bg, tol, bias) {
Some((_, _, boundary)) => start_y as f64 + step as f64 * boundary,
None => fallback,
}
};
let left = probe_h(cx0, mid_y, 1, new_left as f64 - 0.5);
let right = probe_h(cx1, mid_y, -1, new_right as f64 + 0.5);
let top = probe_v(mid_x, cy0, 1, new_top as f64 - 0.5);
let bottom = probe_v(mid_x, cy1, -1, new_bot as f64 + 0.5);
(left, top, right, bottom)
}
#[cfg(test)]
mod tests {
use super::*;
fn solid(width: u32, height: u32, bg: Rgba) -> Vec<u8> {
let mut v = Vec::with_capacity((width * height * 4) as usize);
for _ in 0..(width * height) {
v.extend_from_slice(&[bg.r, bg.g, bg.b, bg.a]);
}
v
}
fn put(buf: &mut [u8], width: u32, x: u32, y: u32, c: Rgba) {
let i = ((y * width + x) * 4) as usize;
buf[i..i + 4].copy_from_slice(&[c.r, c.g, c.b, c.a]);
}
#[test]
fn solid_frame_has_no_edges() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(
&frame,
Px::new(8, 8),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
assert!(edges.iter().all(|e| e.is_none()));
}
#[test]
fn cursor_off_frame_returns_none() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(
&frame,
Px::new(99, 99),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
assert!(edges.iter().all(|e| e.is_none()));
}
#[test]
fn detects_edge_in_each_direction() {
let mut buf = solid(16, 16, Rgba::WHITE);
for y in 0..16 {
put(&mut buf, 16, 11, y, Rgba::BLACK);
}
for x in 0..16 {
put(&mut buf, 16, x, 3, Rgba::BLACK);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(
&frame,
Px::new(8, 8),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
let right = edges[1].expect("right edge");
assert_eq!(right.direction, Direction::Right);
assert_eq!(right.distance, 3);
assert_eq!(right.position, Px::new(11, 8));
assert_eq!(right.edge_color, Rgba::BLACK);
let up = edges[2].expect("up edge");
assert_eq!(up.direction, Direction::Up);
assert_eq!(up.distance, 5);
assert_eq!(up.position, Px::new(8, 3));
assert!(edges[0].is_none(), "left should run off frame");
assert!(edges[3].is_none(), "down should run off frame");
}
#[test]
fn returns_nearest_when_multiple_edges_present() {
let mut buf = solid(16, 16, Rgba::WHITE);
for y in 0..16 {
put(&mut buf, 16, 10, y, Rgba::BLACK);
put(&mut buf, 16, 14, y, Rgba::BLACK);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(
&frame,
Px::new(8, 8),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
let right = edges[1].expect("right");
assert_eq!(right.distance, 2);
assert_eq!(right.position, Px::new(10, 8));
}
#[test]
fn anti_aliased_edge_catches_first_transition() {
let mut buf = solid(16, 16, Rgba::WHITE);
let gray = Rgba::new(180, 180, 180, 255);
for y in 0..16 {
put(&mut buf, 16, 9, y, gray);
put(&mut buf, 16, 10, y, Rgba::BLACK);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(
&frame,
Px::new(7, 8),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
let right = edges[1].expect("right");
assert_eq!(right.position, Px::new(9, 8));
assert_eq!(right.edge_color, gray);
assert_eq!(right.edge_phys, 9.5);
}
#[test]
fn strict_tolerance_skips_subtle_changes() {
let mut buf = solid(16, 16, Rgba::new(200, 200, 200, 255));
let near = Rgba::new(196, 196, 196, 255); for y in 0..16 {
put(&mut buf, 16, 12, y, near);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
assert!(
detect_edges(
&frame,
Px::new(8, 8),
Tolerance::DEFAULT,
EdgeBias::Midpoint
)[1]
.is_none()
);
let edges = detect_edges(&frame, Px::new(8, 8), Tolerance::STRICT, EdgeBias::Midpoint);
assert_eq!(edges[1].expect("strict right").position, Px::new(12, 8));
}
#[test]
fn shrink_fits_inner_content() {
let mut buf = solid(32, 32, Rgba::WHITE);
for y in 14..22 {
for x in 12..20 {
put(&mut buf, 32, x, y, Rgba::BLACK);
}
}
let frame = FrameView::packed(&buf, 32, 32).unwrap();
let (x0, y0, x1, y1) =
shrink_to_content(&frame, 5, 5, 28, 28, Tolerance::DEFAULT, EdgeBias::Midpoint);
assert_eq!((x0, y0, x1, y1), (12, 14, 19, 21));
}
#[test]
fn shrink_returns_original_on_uniform_content() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let r = shrink_to_content(&frame, 2, 2, 14, 14, Tolerance::DEFAULT, EdgeBias::Midpoint);
assert_eq!(r, (2, 2, 14, 14));
}
#[test]
fn shrink_handles_out_of_bounds_rect() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let r = shrink_to_content(
&frame,
-10,
-10,
100,
100,
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
assert_eq!(r, (0, 0, 15, 15));
}
#[test]
fn ignores_alpha_channel() {
let mut buf = solid(16, 16, Rgba::new(120, 120, 120, 255));
let translucent_same = Rgba::new(120, 120, 120, 50);
for y in 0..16 {
put(&mut buf, 16, 11, y, translucent_same);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(
&frame,
Px::new(8, 8),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
assert!(edges[1].is_none());
}
fn soft_block() -> Vec<u8> {
fn level(c: i32) -> u8 {
match c {
13 | 34 => 204, 14 | 33 => 153, 15 | 32 => 102, 16..=31 => 51, _ => 255, }
}
let mut buf = solid(48, 48, Rgba::WHITE);
for y in 0..48i32 {
for x in 0..48i32 {
let v = level(x).max(level(y));
put(&mut buf, 48, x as u32, y as u32, Rgba::new(v, v, v, 255));
}
}
buf
}
#[test]
fn scan_localizes_soft_edge_to_midpoint() {
let buf = soft_block();
let frame = FrameView::packed(&buf, 48, 48).unwrap();
let edges = detect_edges(
&frame,
Px::new(23, 23),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
assert_eq!(edges[0].expect("left").edge_phys, 14.0);
assert_eq!(edges[1].expect("right").edge_phys, 33.0);
assert_eq!(edges[2].expect("up").edge_phys, 14.0);
assert_eq!(edges[3].expect("down").edge_phys, 33.0);
}
#[test]
fn shrink_localizes_soft_edge_to_midpoint() {
let buf = soft_block();
let frame = FrameView::packed(&buf, 48, 48).unwrap();
let (l, t, r, b) =
shrink_to_content_frac(&frame, 2, 2, 45, 45, Tolerance::DEFAULT, EdgeBias::Midpoint);
assert_eq!((l, t, r, b), (14.0, 14.0, 33.0, 33.0));
assert_eq!(r - l, 19.0);
assert_eq!(b - t, 19.0);
}
#[test]
fn soft_edge_modes_agree() {
let buf = soft_block();
let frame = FrameView::packed(&buf, 48, 48).unwrap();
let edges = detect_edges(
&frame,
Px::new(23, 23),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
);
let (l, t, r, b) =
shrink_to_content_frac(&frame, 2, 2, 45, 45, Tolerance::DEFAULT, EdgeBias::Midpoint);
assert_eq!(edges[0].unwrap().edge_phys, l);
assert_eq!(edges[1].unwrap().edge_phys, r);
assert_eq!(edges[2].unwrap().edge_phys, t);
assert_eq!(edges[3].unwrap().edge_phys, b);
}
#[test]
fn crisp_edge_collapses_to_half_pixel_boundary() {
let mut buf = solid(32, 32, Rgba::WHITE);
for y in 0..32 {
for x in 20..32 {
put(&mut buf, 32, x, y, Rgba::BLACK);
}
}
let frame = FrameView::packed(&buf, 32, 32).unwrap();
let right = detect_edges(
&frame,
Px::new(5, 16),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
)[1]
.expect("right edge");
assert_eq!(right.distance, 15);
assert_eq!(right.position, Px::new(20, 16));
assert_eq!(right.edge_phys, 19.5);
let (l, _, r, _) =
shrink_to_content_frac(&frame, 2, 2, 29, 29, Tolerance::DEFAULT, EdgeBias::Midpoint);
assert_eq!(l, 19.5);
assert_eq!(r, 29.5);
}
fn ramp_frame(ramp: &[u8]) -> Vec<u8> {
let mut buf = solid(32, 32, Rgba::WHITE);
for y in 0..32 {
for (i, &v) in ramp.iter().enumerate() {
put(&mut buf, 32, 11 + i as u32, y, Rgba::new(v, v, v, 255));
}
for x in 16..32 {
put(&mut buf, 32, x, y, Rgba::BLACK);
}
}
buf
}
#[test]
fn localizes_asymmetric_ramp_to_brightness_midpoint() {
let buf = ramp_frame(&[51, 30, 16, 8, 3]); let frame = FrameView::packed(&buf, 32, 32).unwrap();
let right = detect_edges(
&frame,
Px::new(2, 16),
Tolerance::DEFAULT,
EdgeBias::Midpoint,
)[1]
.expect("right edge");
assert!((right.edge_phys - 10.625).abs() < 1e-9);
}
#[test]
fn localization_is_independent_of_tolerance() {
let buf = ramp_frame(&[250, 242, 220, 120, 20]); let frame = FrameView::packed(&buf, 32, 32).unwrap();
let at = |tol| {
detect_edges(&frame, Px::new(2, 16), tol, EdgeBias::Midpoint)[1]
.expect("right edge")
.edge_phys
};
let strict = at(Tolerance::STRICT);
assert_eq!(strict, at(Tolerance::DEFAULT));
assert_eq!(strict, at(Tolerance::LOOSE));
assert!((strict - 13.925).abs() < 1e-9);
}
#[test]
fn bias_picks_inner_midpoint_outer_on_soft_edge() {
let buf = soft_block();
let frame = FrameView::packed(&buf, 48, 48).unwrap();
let cursor = Px::new(23, 23);
let inner = detect_edges(&frame, cursor, Tolerance::DEFAULT, EdgeBias::Inner);
let mid = detect_edges(&frame, cursor, Tolerance::DEFAULT, EdgeBias::Midpoint);
let outer = detect_edges(&frame, cursor, Tolerance::DEFAULT, EdgeBias::Outer);
assert_eq!(inner[0].unwrap().edge_phys, 15.5);
assert_eq!(inner[1].unwrap().edge_phys, 31.5);
assert_eq!(mid[0].unwrap().edge_phys, 14.0);
assert_eq!(mid[1].unwrap().edge_phys, 33.0);
assert_eq!(outer[0].unwrap().edge_phys, 12.5);
assert_eq!(outer[1].unwrap().edge_phys, 34.5);
assert_eq!(
inner[1].unwrap().edge_phys - inner[0].unwrap().edge_phys,
16.0
);
assert_eq!(mid[1].unwrap().edge_phys - mid[0].unwrap().edge_phys, 19.0);
assert_eq!(
outer[1].unwrap().edge_phys - outer[0].unwrap().edge_phys,
22.0
);
}
#[test]
fn bias_has_no_effect_on_crisp_edge() {
let mut buf = solid(32, 32, Rgba::WHITE);
for y in 0..32 {
for x in 20..32 {
put(&mut buf, 32, x, y, Rgba::BLACK);
}
}
let frame = FrameView::packed(&buf, 32, 32).unwrap();
let at = |b| {
detect_edges(&frame, Px::new(5, 16), Tolerance::DEFAULT, b)[1]
.unwrap()
.edge_phys
};
assert_eq!(at(EdgeBias::Inner), 19.5);
assert_eq!(at(EdgeBias::Midpoint), 19.5);
assert_eq!(at(EdgeBias::Outer), 19.5);
}
#[test]
fn shrink_crisp_bounds_round_trip_to_inclusive_pixels() {
let mut buf = solid(32, 32, Rgba::WHITE);
for y in 14..22 {
for x in 12..20 {
put(&mut buf, 32, x, y, Rgba::BLACK);
}
}
let frame = FrameView::packed(&buf, 32, 32).unwrap();
let frac =
shrink_to_content_frac(&frame, 5, 5, 28, 28, Tolerance::DEFAULT, EdgeBias::Midpoint);
assert_eq!(frac, (11.5, 13.5, 19.5, 21.5));
assert_eq!(round_bounds(frac), (12, 14, 19, 21));
}
}