use super::region::RegionSet;
use crate::content::graphics_state::{GraphicsStateStack, Matrix};
use crate::content::operators::Operator;
use crate::geometry::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Classification {
Outside,
Inside,
Straddle,
}
impl Classification {
pub fn is_affected(self) -> bool {
!matches!(self, Classification::Outside)
}
}
pub fn transform_bbox(local: &Rect, ctm: &Matrix) -> Rect {
let corners = [
ctm.transform_point(local.left(), local.top()),
ctm.transform_point(local.right(), local.top()),
ctm.transform_point(local.right(), local.bottom()),
ctm.transform_point(local.left(), local.bottom()),
];
let mut x0 = f32::INFINITY;
let mut y0 = f32::INFINITY;
let mut x1 = f32::NEG_INFINITY;
let mut y1 = f32::NEG_INFINITY;
for p in corners {
x0 = x0.min(p.x);
y0 = y0.min(p.y);
x1 = x1.max(p.x);
y1 = y1.max(p.y);
}
Rect::from_points(x0, y0, x1, y1)
}
pub fn classify(
local_bbox: &Rect,
ctm: &Matrix,
regions: &RegionSet,
min_padding: f32,
) -> Classification {
let page = transform_bbox(local_bbox, ctm);
let mut any = false;
let mut all_contained = true;
for r in ®ions.regions {
let padded = r.padded_rect(min_padding);
if padded.intersects(&page) {
any = true;
if !padded.contains_rect(&page) {
all_contained = false;
}
}
}
match (any, all_contained) {
(false, _) => Classification::Outside,
(true, true) => Classification::Inside,
(true, false) => Classification::Straddle,
}
}
pub fn apply_ctm(stack: &mut GraphicsStateStack, op: &Operator) {
match op {
Operator::SaveState => stack.save(),
Operator::RestoreState => stack.restore(),
Operator::Cm { a, b, c, d, e, f } => {
let m = Matrix {
a: *a,
b: *b,
c: *c,
d: *d,
e: *e,
f: *f,
};
let old = stack.current().ctm;
stack.current_mut().ctm = m.multiply(&old);
},
_ => {},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::redaction::region::{RedactionRegion, DEFAULT_EDGE_PADDING};
fn approx_rect(r: &Rect, x0: f32, y0: f32, x1: f32, y1: f32) -> bool {
let e = 1e-3;
(r.left() - x0).abs() < e
&& (r.top() - y0).abs() < e
&& (r.right() - x1).abs() < e
&& (r.bottom() - y1).abs() < e
}
#[test]
fn transform_bbox_identity_is_unchanged() {
let local = Rect::from_points(10.0, 20.0, 30.0, 40.0);
let out = transform_bbox(&local, &Matrix::identity());
assert!(approx_rect(&out, 10.0, 20.0, 30.0, 40.0));
}
#[test]
fn transform_bbox_translation_and_scale() {
let local = Rect::from_points(0.0, 0.0, 10.0, 10.0);
let t = Matrix {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 50.0,
f: 60.0,
};
assert!(approx_rect(&transform_bbox(&local, &t), 50.0, 60.0, 60.0, 70.0));
let s = Matrix {
a: 3.0,
b: 0.0,
c: 0.0,
d: 2.0,
e: 0.0,
f: 0.0,
};
assert!(approx_rect(&transform_bbox(&local, &s), 0.0, 0.0, 30.0, 20.0));
}
#[test]
fn transform_bbox_rotation_envelope() {
let local = Rect::from_points(0.0, 0.0, 2.0, 4.0);
let rot = Matrix {
a: 0.0,
b: 1.0,
c: -1.0,
d: 0.0,
e: 0.0,
f: 0.0,
};
assert!(approx_rect(&transform_bbox(&local, &rot), -4.0, 0.0, 0.0, 2.0));
}
#[test]
fn classify_outside_when_no_region() {
let regions = RegionSet::new(0);
let m = Rect::from_points(0.0, 0.0, 5.0, 5.0);
assert_eq!(
classify(&m, &Matrix::identity(), ®ions, DEFAULT_EDGE_PADDING),
Classification::Outside
);
}
#[test]
fn classify_inside_and_straddle() {
let mut regions = RegionSet::new(0);
regions.push(RedactionRegion::from_rect(100.0, 100.0, 200.0, 200.0, None));
let inside = Rect::from_points(120.0, 120.0, 180.0, 180.0);
assert_eq!(
classify(&inside, &Matrix::identity(), ®ions, DEFAULT_EDGE_PADDING),
Classification::Inside
);
let straddle = Rect::from_points(180.0, 120.0, 260.0, 180.0);
assert_eq!(
classify(&straddle, &Matrix::identity(), ®ions, DEFAULT_EDGE_PADDING),
Classification::Straddle
);
let outside = Rect::from_points(500.0, 500.0, 510.0, 510.0);
assert_eq!(
classify(&outside, &Matrix::identity(), ®ions, DEFAULT_EDGE_PADDING),
Classification::Outside
);
}
#[test]
fn classify_maps_through_scaled_ctm_no_under_redaction() {
let mut regions = RegionSet::new(0);
regions.push(RedactionRegion::from_rect(100.0, 100.0, 200.0, 200.0, None));
let mut stack = GraphicsStateStack::new();
apply_ctm(&mut stack, &Operator::SaveState);
apply_ctm(
&mut stack,
&Operator::Cm {
a: 10.0,
b: 0.0,
c: 0.0,
d: 10.0,
e: 0.0,
f: 0.0,
},
);
let local = Rect::from_points(10.0, 10.0, 20.0, 20.0);
assert_eq!(
classify(&local, &stack.current().ctm, ®ions, DEFAULT_EDGE_PADDING),
Classification::Inside
);
apply_ctm(&mut stack, &Operator::RestoreState);
let id = Matrix::identity();
let c = stack.current().ctm;
assert!(
(c.a - id.a).abs() < 1e-6
&& (c.d - id.d).abs() < 1e-6
&& (c.e - id.e).abs() < 1e-6
&& (c.f - id.f).abs() < 1e-6
);
}
#[test]
fn apply_ctm_nested_concatenation_order() {
let mut stack = GraphicsStateStack::new();
apply_ctm(
&mut stack,
&Operator::Cm {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 100.0,
f: 0.0,
},
);
apply_ctm(
&mut stack,
&Operator::Cm {
a: 2.0,
b: 0.0,
c: 0.0,
d: 2.0,
e: 0.0,
f: 0.0,
},
);
let p = stack.current().ctm.transform_point(5.0, 5.0);
assert!((p.x - 110.0).abs() < 1e-4 && (p.y - 10.0).abs() < 1e-4);
}
#[test]
fn apply_ctm_restore_at_base_is_safe_noop() {
let mut stack = GraphicsStateStack::new();
let d0 = stack.depth();
apply_ctm(&mut stack, &Operator::RestoreState);
apply_ctm(&mut stack, &Operator::RestoreState);
assert_eq!(stack.depth(), d0);
let c = stack.current().ctm;
assert!((c.a - 1.0).abs() < 1e-6 && (c.d - 1.0).abs() < 1e-6);
}
#[test]
fn is_affected_semantics() {
assert!(!Classification::Outside.is_affected());
assert!(Classification::Inside.is_affected());
assert!(Classification::Straddle.is_affected());
}
}