use lopdf::content::Content;
use lopdf::{Document, Object, ObjectId};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rule {
pub x0: f64,
pub y0: f64,
pub x1: f64,
pub y1: f64,
}
impl Rule {
pub fn is_horizontal(&self) -> bool {
(self.x1 - self.x0) > (self.y1 - self.y0)
}
}
const MIN_RULE_LENGTH: f64 = 3.0;
const MAX_RULE_THICKNESS: f64 = 2.5;
const MAX_RULES: usize = 4000;
type Matrix = [f64; 6];
const IDENTITY: Matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
fn mul(m: &Matrix, n: &Matrix) -> Matrix {
[
m[0] * n[0] + m[1] * n[2],
m[0] * n[1] + m[1] * n[3],
m[2] * n[0] + m[3] * n[2],
m[2] * n[1] + m[3] * n[3],
m[4] * n[0] + m[5] * n[2] + n[4],
m[4] * n[1] + m[5] * n[3] + n[5],
]
}
fn apply(m: &Matrix, x: f64, y: f64) -> (f64, f64) {
(m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5])
}
fn num(o: &Object) -> Option<f64> {
match o {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(f64::from(*r)),
_ => None,
}
}
#[derive(Clone, Copy)]
struct Seg {
a: (f64, f64),
b: (f64, f64),
from_rect: bool,
}
pub fn page_rules(doc: &Document, page_id: ObjectId) -> Vec<Rule> {
let data = doc.get_page_content(page_id);
let Ok(content) = Content::decode(&data) else {
return Vec::new();
};
let mut rules: Vec<Rule> = Vec::new();
let mut ctm = IDENTITY;
let mut stack: Vec<Matrix> = Vec::new();
let mut path: Vec<Seg> = Vec::new();
let mut cur: Option<(f64, f64)> = None;
let mut start: Option<(f64, f64)> = None;
for op in &content.operations {
let args: Vec<f64> = op.operands.iter().filter_map(num).collect();
match op.operator.as_str() {
"q" => stack.push(ctm),
"Q" => {
if let Some(m) = stack.pop() {
ctm = m;
}
}
"cm" if args.len() == 6 => {
let m = [args[0], args[1], args[2], args[3], args[4], args[5]];
ctm = mul(&m, &ctm);
}
"m" if args.len() == 2 => {
let p = apply(&ctm, args[0], args[1]);
cur = Some(p);
start = Some(p);
}
"l" if args.len() == 2 => {
let p = apply(&ctm, args[0], args[1]);
if let Some(c) = cur {
path.push(Seg {
a: c,
b: p,
from_rect: false,
});
}
cur = Some(p);
}
"h" => {
if let (Some(c), Some(s)) = (cur, start) {
path.push(Seg {
a: c,
b: s,
from_rect: false,
});
cur = Some(s);
}
}
"re" if args.len() == 4 => {
let (x, y, w, h) = (args[0], args[1], args[2], args[3]);
let p0 = apply(&ctm, x, y);
let p1 = apply(&ctm, x + w, y);
let p2 = apply(&ctm, x + w, y + h);
let p3 = apply(&ctm, x, y + h);
for (a, b) in [(p0, p1), (p1, p2), (p2, p3), (p3, p0)] {
path.push(Seg {
a,
b,
from_rect: true,
});
}
cur = Some(p0);
start = Some(p0);
}
"S" | "s" | "B" | "B*" | "b" | "b*" => {
collect(&path, true, &mut rules);
path.clear();
cur = None;
}
"f" | "F" | "f*" => {
collect(&path, false, &mut rules);
path.clear();
cur = None;
}
"n" => {
path.clear();
cur = None;
}
_ => {}
}
if rules.len() > MAX_RULES {
return Vec::new();
}
}
rules
}
fn collect(path: &[Seg], stroked: bool, out: &mut Vec<Rule>) {
if path.is_empty() {
return;
}
if !stroked {
if !path.iter().all(|s| s.from_rect) {
return;
}
for rect in path.chunks_exact(4) {
let xs = [rect[0].a.0, rect[1].a.0, rect[2].a.0, rect[3].a.0];
let ys = [rect[0].a.1, rect[1].a.1, rect[2].a.1, rect[3].a.1];
let (x0, x1) = min_max(&xs);
let (y0, y1) = min_max(&ys);
let (w, h) = (x1 - x0, y1 - y0);
if h <= MAX_RULE_THICKNESS && w >= MIN_RULE_LENGTH {
let y = (y0 + y1) / 2.0;
out.push(Rule {
x0,
y0: y,
x1,
y1: y,
});
} else if w <= MAX_RULE_THICKNESS && h >= MIN_RULE_LENGTH {
let x = (x0 + x1) / 2.0;
out.push(Rule {
x0: x,
y0,
x1: x,
y1,
});
}
}
return;
}
for s in path {
let (dx, dy) = ((s.b.0 - s.a.0).abs(), (s.b.1 - s.a.1).abs());
if dy < 0.5 && dx >= MIN_RULE_LENGTH {
out.push(Rule {
x0: s.a.0.min(s.b.0),
y0: s.a.1,
x1: s.a.0.max(s.b.0),
y1: s.a.1,
});
} else if dx < 0.5 && dy >= MIN_RULE_LENGTH {
out.push(Rule {
x0: s.a.0,
y0: s.a.1.min(s.b.1),
x1: s.a.0,
y1: s.a.1.max(s.b.1),
});
}
}
}
fn min_max(v: &[f64]) -> (f64, f64) {
v.iter()
.fold((f64::MAX, f64::MIN), |(lo, hi), &x| (lo.min(x), hi.max(x)))
}