use std::collections::HashSet;
use crate::types::{ItemType, PdfRect, TextItem};
const MAX_RULE_THICKNESS: f32 = 2.0;
const MIN_X_OVERLAP: f32 = 0.6;
const MIN_REPEATED_RULE_LEVELS: usize = 3;
const RULE_Y_DEDUP_EPS: f32 = 2.0;
const RULE_SPAN_OVERLAP_RATIO: f32 = 0.8;
const RULE_SPAN_WIDTH_RATIO: f32 = 1.5;
const MIN_SEGMENTED_ROW_RULES: usize = 3;
const MIN_SEGMENTED_ROW_GAPS: usize = 2;
const SEGMENTED_ROW_GAP_MIN: f32 = 12.0;
const MIN_TABULAR_RULE_ITEMS: usize = 3;
const MIN_TABULAR_RULE_GAPS: usize = 2;
const TABULAR_RULE_GAP_EM: f32 = 2.0;
const STRIKE_OWNER_PAD_EM: f32 = 0.75;
const STRIKE_OWNER_MIN_PAD: f32 = 4.0;
const STRIKE_ROW_Y_TOLERANCE_EM: f32 = 0.15;
const STRIKE_ROW_Y_TOLERANCE_MIN: f32 = 5.0;
const GRAPHIC_CONNECTION_EPS: f32 = 2.0;
const GRAPHIC_CONNECTOR_MAX_THICKNESS: f32 = 4.0;
#[derive(Clone)]
pub(crate) struct UnderlineLine {
pub(crate) x1: f32,
pub(crate) y1: f32,
pub(crate) x2: f32,
pub(crate) y2: f32,
pub(crate) stroke_width: f32,
pub(crate) page: u32,
}
#[derive(Clone)]
struct Rule {
x1: f32,
x2: f32,
y: f32,
}
impl Rule {
fn width(&self) -> f32 {
self.x2 - self.x1
}
}
fn rules_from_graphics(rects: &[PdfRect], lines: &[UnderlineLine], page: u32) -> Vec<Rule> {
let mut rules: Vec<Rule> = Vec::new();
for l in lines {
if l.page != page {
continue;
}
if l.stroke_width <= MAX_RULE_THICKNESS && (l.y1 - l.y2).abs() <= MAX_RULE_THICKNESS {
let (x1, x2) = if l.x1 <= l.x2 {
(l.x1, l.x2)
} else {
(l.x2, l.x1)
};
if x2 - x1 > 1.0 {
rules.push(Rule {
x1,
x2,
y: (l.y1 + l.y2) / 2.0,
});
}
}
}
for r in rects {
if r.page != page {
continue;
}
let (x1, x2) = if r.width >= 0.0 {
(r.x, r.x + r.width)
} else {
(r.x + r.width, r.x)
};
if r.height.abs() <= MAX_RULE_THICKNESS && x2 - x1 > 1.0 {
rules.push(Rule {
x1,
x2,
y: r.y + r.height / 2.0,
});
}
}
rules
}
fn discard_repeated_ruling_rules(
rules: Vec<Rule>,
items: &[TextItem],
rects: &[PdfRect],
lines: &[UnderlineLine],
page: u32,
) -> Vec<Rule> {
if rules.len() < MIN_REPEATED_RULE_LEVELS {
return rules;
}
rules
.iter()
.filter(|rule| {
!is_segmented_row_ruling_rule(rule, &rules)
&& ((has_snug_text_owner(rule, items)
&& !has_flanking_verticals(rule, rects, lines, page))
|| !is_repeated_ruling_rule(rule, &rules))
})
.cloned()
.collect()
}
fn has_flanking_verticals(
rule: &Rule,
rects: &[PdfRect],
lines: &[UnderlineLine],
page: u32,
) -> bool {
let norm = |r: &PdfRect| {
let (x_lo, x_hi) = if r.width >= 0.0 {
(r.x, r.x + r.width)
} else {
(r.x + r.width, r.x)
};
let (y_lo, y_hi) = if r.height >= 0.0 {
(r.y, r.y + r.height)
} else {
(r.y + r.height, r.y)
};
(x_lo, x_hi, y_lo, y_hi)
};
let page_rects: Vec<(f32, f32, f32, f32)> = rects
.iter()
.filter(|r| r.page == page && r.height.abs() > 6.0)
.map(norm)
.collect();
let rect_flank = page_rects.iter().any(|&(x_lo, x_hi, y_lo, y_hi)| {
let contains = x_lo <= rule.x1 + 2.0
&& x_hi >= rule.x2 - 2.0
&& y_lo <= rule.y + 2.0
&& y_hi >= rule.y - 2.0;
if !contains {
return false;
}
page_rects.iter().any(|&(nx_lo, nx_hi, ny_lo, ny_hi)| {
let x_overlap = nx_hi.min(x_hi) - nx_lo.max(x_lo);
if x_overlap <= 10.0 {
return false;
}
(ny_lo - y_hi).abs() <= 3.0 || (y_lo - ny_hi).abs() <= 3.0
})
});
if rect_flank {
return true;
}
lines.iter().any(|l| {
if l.page != page || (l.x1 - l.x2).abs() > 2.0 {
return false;
}
let x = (l.x1 + l.x2) / 2.0;
let near_end = (x - rule.x1).abs() <= 6.0 || (x - rule.x2).abs() <= 6.0;
if !near_end {
return false;
}
let (y_lo, y_hi) = if l.y1 <= l.y2 {
(l.y1, l.y2)
} else {
(l.y2, l.y1)
};
y_lo <= rule.y + 2.0 && y_hi >= rule.y - 2.0
})
}
fn has_snug_text_owner(rule: &Rule, items: &[TextItem]) -> bool {
let matched: Vec<&TextItem> = items
.iter()
.filter(|item| is_underline_candidate(item) && rule_matches_item(rule, item))
.collect();
if matched.is_empty() {
return false;
}
let x1 = matched.iter().map(|i| i.x).fold(f32::INFINITY, f32::min);
let x2 = matched
.iter()
.map(|i| i.x + i.width)
.fold(f32::NEG_INFINITY, f32::max);
let max_fs = matched.iter().map(|i| i.font_size).fold(0.0, f32::max);
let pad = (max_fs * 0.75).max(4.0);
if rule.x1 < x1 - pad || rule.x2 > x2 + pad {
return false;
}
let covered: f32 = matched.iter().map(|i| i.width).sum();
if covered < rule.width() * 0.6 {
return false;
}
let mut sorted = matched;
sorted.sort_by(|a, b| a.x.total_cmp(&b.x));
sorted.windows(2).all(|pair| {
let gap = pair[1].x - (pair[0].x + pair[0].width);
gap <= (max_fs * 2.0).max(12.0)
})
}
fn is_repeated_ruling_rule(rule: &Rule, rules: &[Rule]) -> bool {
let mut y_levels: Vec<f32> = rules
.iter()
.filter(|other| has_similar_span(rule, other))
.map(|other| other.y)
.collect();
y_levels.sort_by(|a, b| a.total_cmp(b));
y_levels.dedup_by(|a, b| (*a - *b).abs() <= RULE_Y_DEDUP_EPS);
y_levels.len() >= MIN_REPEATED_RULE_LEVELS
}
fn is_segmented_row_ruling_rule(rule: &Rule, rules: &[Rule]) -> bool {
let mut row_rules: Vec<&Rule> = rules
.iter()
.filter(|other| (other.y - rule.y).abs() <= RULE_Y_DEDUP_EPS)
.collect();
if row_rules.len() < MIN_SEGMENTED_ROW_RULES {
return false;
}
row_rules.sort_by(|a, b| a.x1.total_cmp(&b.x1));
let large_gaps = row_rules
.windows(2)
.filter(|pair| pair[1].x1 - pair[0].x2 > SEGMENTED_ROW_GAP_MIN)
.count();
large_gaps >= MIN_SEGMENTED_ROW_GAPS
}
fn has_similar_span(a: &Rule, b: &Rule) -> bool {
let a_width = a.width();
let b_width = b.width();
if a_width <= 1.0 || b_width <= 1.0 {
return false;
}
let width_ratio = a_width.max(b_width) / a_width.min(b_width);
if width_ratio > RULE_SPAN_WIDTH_RATIO {
return false;
}
let overlap = a.x2.min(b.x2) - a.x1.max(b.x1);
overlap >= a_width.min(b_width) * RULE_SPAN_OVERLAP_RATIO
}
fn tabular_row_separator_rule_indices(rules: &[Rule], items: &[TextItem]) -> HashSet<usize> {
let mut tabular_rules = HashSet::new();
for (rule_idx, rule) in rules.iter().enumerate() {
let mut matched_items: Vec<&TextItem> = items
.iter()
.filter(|item| is_underline_candidate(item) && rule_matches_item(rule, item))
.collect();
if matched_items.len() < MIN_TABULAR_RULE_ITEMS {
continue;
}
matched_items.sort_by(|a, b| a.x.total_cmp(&b.x));
let large_gaps = matched_items
.windows(2)
.filter(|pair| {
let left = pair[0];
let right = pair[1];
let gap = right.x - (left.x + left.width);
let font_size = left.font_size.max(right.font_size).max(1.0);
gap > font_size * TABULAR_RULE_GAP_EM
})
.count();
if large_gaps >= MIN_TABULAR_RULE_GAPS {
tabular_rules.insert(rule_idx);
}
}
tabular_rules
}
fn is_underline_candidate(item: &TextItem) -> bool {
matches!(item.item_type, ItemType::Text)
&& !item.text.trim().is_empty()
&& item.width > 0.0
&& item.advance_known
&& has_level_baseline(item)
}
fn has_level_baseline(item: &TextItem) -> bool {
const SKEW_TOLERANCE_DEG: f32 = 5.0;
let r = item.rotation;
r <= SKEW_TOLERANCE_DEG
|| r >= 360.0 - SKEW_TOLERANCE_DEG
|| (r - 180.0).abs() <= SKEW_TOLERANCE_DEG
}
fn baseline_and_up(item: &TextItem) -> (f32, f32) {
if item.is_upside_down() {
(item.y + item.height, -1.0)
} else {
(item.y, 1.0)
}
}
fn rule_in_glyph_band(rule_y: f32, item: &TextItem, lo: f32, hi: f32) -> bool {
let (base, up) = baseline_and_up(item);
let a = base + up * lo;
let b = base + up * hi;
rule_y >= a.min(b) && rule_y <= a.max(b)
}
fn rule_matches_item(rule: &Rule, item: &TextItem) -> bool {
let below = (item.font_size * 0.72).max(3.0);
if !rule_in_glyph_band(rule.y, item, -below, 1.0) {
return false;
}
let ix1 = item.x;
let ix2 = item.x + item.width;
let min_overlap = item.width * MIN_X_OVERLAP;
let overlap = rule.x2.min(ix2) - rule.x1.max(ix1);
overlap >= min_overlap
}
fn rule_strikes_item(rule: &Rule, item: &TextItem) -> bool {
if !rule_in_glyph_band(rule.y, item, item.font_size * 0.12, item.font_size * 0.55) {
return false;
}
let ix1 = item.x;
let ix2 = item.x + item.width;
let min_overlap = item.width * MIN_X_OVERLAP;
let overlap = rule.x2.min(ix2) - rule.x1.max(ix1);
overlap >= min_overlap
}
fn is_bare_list_marker(text: &str) -> bool {
matches!(
text.trim(),
"•" | "◦" | "▪" | "▫" | "‣" | "⁃" | "●" | "○" | "■" | "□" | "-" | "*"
)
}
fn same_strike_row(left: &TextItem, right: &TextItem) -> bool {
let font_size = left.font_size.max(right.font_size);
let tolerance = (font_size * STRIKE_ROW_Y_TOLERANCE_EM).max(STRIKE_ROW_Y_TOLERANCE_MIN);
left.is_upside_down() == right.is_upside_down()
&& (baseline_and_up(left).0 - baseline_and_up(right).0).abs() <= tolerance
}
fn is_inline_script(rule: &Rule, candidate: &TextItem, parent: &TextItem) -> bool {
if !candidate.is_upright()
|| !parent.is_upright()
|| !is_underline_candidate(candidate)
|| is_bare_list_marker(&candidate.text)
|| candidate.font_size <= 0.0
|| candidate.font_size >= parent.font_size * 0.75
|| candidate.text.len() > 4
|| !candidate.text.chars().all(|c| c.is_ascii_digit())
|| (candidate.y - parent.y).abs() > 5.0
{
return false;
}
let parent_ends_with_letter = parent.text.chars().last().is_some_and(char::is_alphabetic);
if !parent_ends_with_letter {
return false;
}
let parent_right = parent.x + parent.width;
let gap = candidate.x - parent_right;
if gap >= parent.font_size * 0.2 || gap <= -parent.font_size * 0.3 {
return false;
}
let overlap = rule.x2.min(candidate.x + candidate.width) - rule.x1.max(candidate.x);
overlap >= candidate.width * MIN_X_OVERLAP
}
fn snug_strike_owner_indices(rule: &Rule, items: &[TextItem]) -> Vec<usize> {
let mut struck_indices: Vec<usize> = items
.iter()
.enumerate()
.filter_map(|(index, candidate)| {
(is_underline_candidate(candidate)
&& !is_bare_list_marker(&candidate.text)
&& rule_strikes_item(rule, candidate))
.then_some(index)
})
.collect();
if struck_indices.is_empty() {
return Vec::new();
}
struck_indices.sort_by(|&left, &right| {
baseline_and_up(&items[left])
.0
.total_cmp(&baseline_and_up(&items[right]).0)
});
let mut rows: Vec<Vec<usize>> = Vec::new();
for index in struck_indices {
if let Some(row) = rows
.last_mut()
.filter(|row| same_strike_row(&items[row[0]], &items[index]))
{
row.push(index);
} else {
rows.push(vec![index]);
}
}
let mut owned_indices = Vec::new();
for mut row in rows {
row.sort_by(|&left, &right| items[left].x.total_cmp(&items[right].x));
let scripts: Vec<usize> = items
.iter()
.enumerate()
.filter_map(|(index, candidate)| {
let parent_pos =
row.partition_point(|&row_index| items[row_index].x <= candidate.x);
let parent_index = parent_pos.checked_sub(1).map(|pos| row[pos])?;
is_inline_script(rule, candidate, &items[parent_index]).then_some(index)
})
.collect();
row.extend(scripts);
row.sort_by(|&left, &right| items[left].x.total_cmp(&items[right].x));
row.dedup();
let x1 = row
.iter()
.map(|&index| items[index].x)
.fold(f32::INFINITY, f32::min);
let x2 = row
.iter()
.map(|&index| items[index].x + items[index].width)
.fold(f32::NEG_INFINITY, f32::max);
let max_font_size = row
.iter()
.map(|&index| items[index].font_size)
.fold(0.0, f32::max);
let pad = (max_font_size * STRIKE_OWNER_PAD_EM).max(STRIKE_OWNER_MIN_PAD);
if rule.x1 < x1 - pad || rule.x2 > x2 + pad {
continue;
}
let contiguous = row.windows(2).all(|pair| {
let gap = items[pair[1]].x - (items[pair[0]].x + items[pair[0]].width);
gap <= (max_font_size * 2.0).max(12.0)
});
if contiguous {
owned_indices.extend(row);
}
}
owned_indices.sort_unstable();
owned_indices.dedup();
owned_indices
}
fn has_connected_nonhorizontal_segment(rule: &Rule, lines: &[UnderlineLine], page: u32) -> bool {
lines.iter().any(|line| {
if line.page != page {
return false;
}
let dx = line.x2 - line.x1;
let dy = line.y2 - line.y1;
if dy.abs() <= MAX_RULE_THICKNESS {
return false;
}
let y_min = line.y1.min(line.y2) - GRAPHIC_CONNECTION_EPS;
let y_max = line.y1.max(line.y2) + GRAPHIC_CONNECTION_EPS;
if rule.y < y_min || rule.y > y_max {
return false;
}
let t = (rule.y - line.y1) / dy;
if !(-0.05..=1.05).contains(&t) {
return false;
}
let intersection_x = line.x1 + t * dx;
intersection_x >= rule.x1 - GRAPHIC_CONNECTION_EPS
&& intersection_x <= rule.x2 + GRAPHIC_CONNECTION_EPS
})
}
fn has_connected_nonhorizontal_rect(rule: &Rule, rects: &[PdfRect], page: u32) -> bool {
rects.iter().any(|rect| {
if rect.page != page {
return false;
}
let (x1, x2) = if rect.width >= 0.0 {
(rect.x, rect.x + rect.width)
} else {
(rect.x + rect.width, rect.x)
};
let (y1, y2) = if rect.height >= 0.0 {
(rect.y, rect.y + rect.height)
} else {
(rect.y + rect.height, rect.y)
};
let width = x2 - x1;
let height = y2 - y1;
width > 0.0
&& width <= GRAPHIC_CONNECTOR_MAX_THICKNESS
&& height > width * 2.0
&& rule.y >= y1 - GRAPHIC_CONNECTION_EPS
&& rule.y <= y2 + GRAPHIC_CONNECTION_EPS
&& x2 >= rule.x1 - GRAPHIC_CONNECTION_EPS
&& x1 <= rule.x2 + GRAPHIC_CONNECTION_EPS
})
}
pub(crate) fn mark_underlined_items(
items: &mut [TextItem],
rects: &[PdfRect],
lines: &[UnderlineLine],
page: u32,
) {
let rules = discard_repeated_ruling_rules(
rules_from_graphics(rects, lines, page),
items,
rects,
lines,
page,
);
if rules.is_empty() {
return;
}
let tabular_rules = tabular_row_separator_rule_indices(&rules, items);
let fraction_rules: HashSet<usize> = rules
.iter()
.enumerate()
.filter(|(_, rule)| {
rule.width() <= 60.0
&& items.iter().any(|item| {
if !is_underline_candidate(item) || !item.is_upright() {
return false;
}
let dy = rule.y - (item.y + item.height);
let overlap = rule.x2.min(item.x + item.width) - rule.x1.max(item.x);
dy > 0.0
&& dy <= item.font_size * 0.3
&& overlap > rule.width() * 0.5
&& item.width <= rule.width() * 1.5
})
})
.map(|(i, _)| i)
.collect();
let mut strikeout_items = vec![false; items.len()];
for (rule_idx, rule) in rules.iter().enumerate() {
if tabular_rules.contains(&rule_idx)
|| has_connected_nonhorizontal_segment(rule, lines, page)
|| has_connected_nonhorizontal_rect(rule, rects, page)
{
continue;
}
for item_idx in snug_strike_owner_indices(rule, items) {
strikeout_items[item_idx] = true;
}
}
let underlined_items: HashSet<usize> = items
.iter()
.enumerate()
.filter(|(_, item)| {
is_underline_candidate(item)
&& rules.iter().enumerate().any(|(rule_idx, rule)| {
!tabular_rules.contains(&rule_idx)
&& !fraction_rules.contains(&rule_idx)
&& rule_matches_item(rule, item)
})
})
.map(|(item_idx, _)| item_idx)
.collect();
for (item_idx, item) in items.iter_mut().enumerate() {
if !is_underline_candidate(item) {
continue;
}
if strikeout_items[item_idx] {
item.is_strikeout = true;
}
if underlined_items.contains(&item_idx) {
item.is_underline = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ItemType;
fn item(text: &str, x: f32, y: f32, width: f32, font_size: f32) -> TextItem {
TextItem {
text: text.to_string(),
x,
y,
width,
height: font_size,
font: "F1".to_string(),
font_tag: String::new(),
legacy_symbol_rewrite: false,
font_size,
page: 1,
is_bold: false,
is_italic: false,
font_weight: None,
bold_source: None,
fixed_pitch: None,
is_underline: false,
is_strikeout: false,
rotation: 0.0,
advance_known: true,
item_type: ItemType::Text,
mcid: None,
baseline_shift: 0.0,
}
}
fn hline(x1: f32, x2: f32, y: f32) -> UnderlineLine {
UnderlineLine {
x1,
y1: y,
x2,
y2: y,
stroke_width: 1.0,
page: 1,
}
}
fn cell_rect(x: f32, y: f32, width: f32, height: f32) -> PdfRect {
PdfRect {
x,
y,
width,
height,
page: 1,
}
}
fn thin_rect(x: f32, y: f32, width: f32) -> PdfRect {
PdfRect {
x,
y,
width,
height: 0.8,
page: 1,
}
}
#[test]
fn stroked_line_under_baseline_marks_underline() {
let mut items = vec![item("underlined", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(99.0, 161.0, 498.5)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_underline);
}
#[test]
fn thin_filled_rect_under_baseline_marks_underline() {
let mut items = vec![item("underlined", 100.0, 500.0, 60.0, 10.0)];
let rects = vec![thin_rect(100.0, 497.8, 60.0)];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(items[0].is_underline);
}
#[test]
fn long_rule_under_multiple_items_marks_each() {
let mut items = vec![
item("first", 100.0, 500.0, 40.0, 10.0),
item("second", 145.0, 500.0, 50.0, 10.0),
];
let lines = vec![hline(98.0, 200.0, 498.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_underline);
assert!(items[1].is_underline);
}
#[test]
fn line_far_below_baseline_is_not_an_underline() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(90.0, 300.0, 470.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
}
#[test]
fn thick_stroked_line_is_not_an_underline() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let mut line = hline(99.0, 161.0, 498.5);
line.stroke_width = 4.0;
mark_underlined_items(&mut items, &[], &[line], 1);
assert!(!items[0].is_underline);
}
#[test]
fn mid_glyph_rule_marks_strikeout_not_underline() {
let mut items = vec![item("struck out", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(99.0, 161.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_strikeout);
assert!(!items[0].is_underline);
}
#[test]
fn long_connector_crossing_text_is_not_a_strikeout() {
let mut items = vec![item("diagram label", 160.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(100.0, 280.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_strikeout);
}
#[test]
fn chart_rule_ending_inside_short_label_is_not_a_strikeout() {
let mut items = vec![item("T 18", 300.0, 500.0, 20.0, 10.0)];
let lines = vec![hline(100.0, 315.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_strikeout);
}
#[test]
fn connected_diagram_segment_is_not_a_strikeout() {
let mut items = vec![item("V8", 100.0, 500.0, 12.0, 10.0)];
let lines = vec![
hline(99.0, 113.0, 503.0),
UnderlineLine {
x1: 106.0,
y1: 496.0,
x2: 109.0,
y2: 510.0,
stroke_width: 1.0,
page: 1,
},
];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_strikeout);
}
#[test]
fn connected_filled_rect_is_not_a_strikeout() {
let mut items = vec![item("V8", 100.0, 500.0, 12.0, 10.0)];
let rects = vec![
thin_rect(99.0, 502.6, 14.0),
PdfRect {
x: 106.0,
y: 496.0,
width: 2.0,
height: 14.0,
page: 1,
},
];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(!items[0].is_strikeout);
}
#[test]
fn broad_fill_behind_text_does_not_block_strikeout() {
let mut items = vec![item("deleted", 100.0, 500.0, 40.0, 10.0)];
let rects = vec![
thin_rect(99.0, 502.6, 42.0),
PdfRect {
x: 90.0,
y: 490.0,
width: 100.0,
height: 20.0,
page: 1,
},
];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(items[0].is_strikeout);
}
#[test]
fn list_bullet_is_not_a_strikeout() {
for marker in ["•", "-", "*"] {
let mut items = vec![item(marker, 100.0, 500.0, 6.0, 10.0)];
let lines = vec![hline(99.0, 107.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_strikeout, "marker {marker:?}");
}
}
#[test]
fn snug_rule_marks_adjacent_split_runs_as_strikeout() {
let mut items = vec![
item("deleted", 100.0, 500.0, 40.0, 10.0),
item("text", 142.0, 500.0, 25.0, 10.0),
];
let lines = vec![hline(99.0, 168.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| item.is_strikeout));
}
#[test]
fn snug_rule_groups_split_runs_with_baseline_drift() {
let mut items = vec![
item("deleted", 100.0, 500.0, 40.0, 10.0),
item("text", 142.0, 498.0, 25.0, 10.0),
];
let lines = vec![hline(99.0, 168.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| item.is_strikeout));
}
#[test]
fn snug_rule_keeps_inline_superscript_in_strike_owner() {
let mut items = vec![
item("deleted", 100.0, 500.0, 40.0, 10.0),
item("2", 140.5, 503.0, 4.0, 6.0),
];
let lines = vec![hline(99.0, 145.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| item.is_strikeout));
}
#[test]
fn snug_rule_keeps_inline_subscript_in_strike_owner() {
let mut items = vec![
item("deleted", 100.0, 500.0, 40.0, 10.0),
item("2", 140.5, 497.0, 4.0, 6.0),
];
let lines = vec![hline(99.0, 145.0, 503.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| item.is_strikeout));
}
#[test]
fn baseline_rule_marks_underline_not_strikeout() {
let mut items = vec![item("underlined", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(99.0, 161.0, 498.5)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_underline);
assert!(!items[0].is_strikeout);
}
#[test]
fn overline_is_neither_underline_nor_strikeout() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(99.0, 161.0, 507.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
assert!(!items[0].is_strikeout);
}
#[test]
fn thin_filled_rect_at_mid_glyph_marks_strikeout() {
let mut items = vec![item("struck out", 100.0, 500.0, 60.0, 10.0)];
let rects = vec![thin_rect(100.0, 502.6, 60.0)];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(items[0].is_strikeout);
assert!(!items[0].is_underline);
}
#[test]
fn line_above_baseline_is_not_an_underline() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![hline(90.0, 300.0, 505.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
}
#[test]
fn insufficient_horizontal_overlap_is_not_an_underline() {
let mut items = vec![item("wide text item", 100.0, 500.0, 100.0, 10.0)];
let lines = vec![hline(100.0, 125.0, 498.5)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
}
#[test]
fn negative_width_rect_is_normalized_and_marks_underline() {
let mut items = vec![item("underlined", 100.0, 500.0, 60.0, 10.0)];
let rects = vec![PdfRect {
x: 160.0,
y: 497.8,
width: -60.0,
height: 0.8,
page: 1,
}];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(items[0].is_underline);
}
#[test]
fn negative_height_band_is_not_an_underline() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let rects = vec![PdfRect {
x: 95.0,
y: 509.0,
width: 80.0,
height: -14.0,
page: 1,
}];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(!items[0].is_underline);
}
#[test]
fn thick_band_is_not_an_underline() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let rects = vec![PdfRect {
x: 95.0,
y: 495.0,
width: 80.0,
height: 14.0,
page: 1,
}];
mark_underlined_items(&mut items, &rects, &[], 1);
assert!(!items[0].is_underline);
}
#[test]
fn vertical_line_is_not_an_underline() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let lines = vec![UnderlineLine {
x1: 120.0,
y1: 498.0,
x2: 120.0,
y2: 400.0,
stroke_width: 1.0,
page: 1,
}];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
}
#[test]
fn other_pages_graphics_do_not_mark() {
let mut items = vec![item("text", 100.0, 500.0, 60.0, 10.0)];
let mut line = hline(99.0, 161.0, 498.5);
line.page = 2;
mark_underlined_items(&mut items, &[], &[line], 1);
assert!(!items[0].is_underline);
}
#[test]
fn repeated_table_row_rules_do_not_mark_cell_text() {
let mut items = vec![
item("A", 110.0, 500.0, 20.0, 10.0),
item("B", 110.0, 480.0, 20.0, 10.0),
item("C", 110.0, 460.0, 20.0, 10.0),
];
let lines = vec![
hline(100.0, 150.0, 498.0),
hline(100.0, 150.0, 478.0),
hline(100.0, 150.0, 458.0),
];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| !item.is_underline));
}
#[test]
fn row_separator_under_spaced_column_labels_is_not_an_underline() {
let mut items = vec![
item("Date", 100.0, 500.0, 25.0, 10.0),
item("Rate", 200.0, 500.0, 25.0, 10.0),
item("Yield", 300.0, 500.0, 30.0, 10.0),
];
let lines = vec![hline(90.0, 340.0, 498.0)];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| !item.is_underline));
}
#[test]
fn repeated_snug_underlines_survive_ruling_filter() {
let mut items = vec![
item("first underlined line of text", 50.0, 700.0, 300.0, 11.0),
item("second underlined line here", 50.0, 650.0, 300.0, 11.0),
item("third underlined line as well", 50.0, 600.0, 300.0, 11.0),
];
let lines = vec![
hline(50.0, 350.0, 697.0),
hline(50.0, 350.0, 647.0),
hline(50.0, 350.0, 597.0),
];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| item.is_underline));
}
#[test]
fn snug_rescue_spans_split_runs_on_one_line() {
let mut items = vec![
item("run one", 50.0, 700.0, 100.0, 11.0),
item("run two", 150.5, 700.0, 100.0, 11.0),
item("run three", 251.0, 700.0, 99.0, 11.0),
item("other a", 50.0, 650.0, 300.0, 11.0),
item("other b", 50.0, 600.0, 300.0, 11.0),
];
let lines = vec![
hline(50.0, 350.0, 697.0),
hline(50.0, 350.0, 647.0),
hline(50.0, 350.0, 597.0),
];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_underline && items[1].is_underline && items[2].is_underline);
}
#[test]
fn snug_rescue_denied_for_row_with_cell_gaps() {
let mut items = vec![
item("cell a", 50.0, 700.0, 60.0, 11.0),
item("cell b", 190.0, 700.0, 60.0, 11.0),
item("cell c", 330.0, 700.0, 70.0, 11.0),
item("cell d", 50.0, 650.0, 60.0, 11.0),
item("cell e", 190.0, 650.0, 60.0, 11.0),
item("cell f", 330.0, 650.0, 70.0, 11.0),
];
let lines = vec![
hline(50.0, 400.0, 697.0),
hline(50.0, 400.0, 647.0),
hline(50.0, 400.0, 597.0),
];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| !item.is_underline));
}
#[test]
fn snug_rescue_denied_inside_cell_box() {
let mut items = vec![
item("one wide cell row", 50.0, 700.0, 300.0, 11.0),
item("second wide cell", 50.0, 650.0, 300.0, 11.0),
item("third wide cell", 50.0, 600.0, 300.0, 11.0),
];
let lines = vec![
hline(50.0, 350.0, 697.0),
hline(50.0, 350.0, 647.0),
hline(50.0, 350.0, 597.0),
];
let boxes = vec![
cell_rect(45.0, 690.0, 320.0, 50.0),
cell_rect(45.0, 640.0, 320.0, 50.0),
cell_rect(45.0, 590.0, 320.0, 50.0),
];
mark_underlined_items(&mut items, &boxes, &lines, 1);
assert!(items.iter().all(|item| !item.is_underline));
}
#[test]
fn same_row_spaced_rule_segments_do_not_mark_column_labels() {
let mut items = vec![
item("Date", 100.0, 500.0, 25.0, 10.0),
item("Rate", 200.0, 500.0, 25.0, 10.0),
item("Yield", 300.0, 500.0, 30.0, 10.0),
];
let lines = vec![
hline(98.0, 128.0, 498.0),
hline(198.0, 228.0, 498.0),
hline(298.0, 333.0, 498.0),
];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items.iter().all(|item| !item.is_underline));
}
#[test]
fn vertical_run_is_never_underlined_by_a_rule_under_its_box() {
let mut vertical = item("Header", 100.0, 500.0, 10.0, 10.0);
vertical.height = 60.0;
vertical.rotation = 90.0;
let horizontal = item("Header", 100.0, 500.0, 10.0, 10.0);
let lines = vec![hline(98.0, 112.0, 498.5)];
let mut items = vec![vertical];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
let mut items = vec![horizontal];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_underline);
let mut flipped = item("Header", 100.0, 500.0, 10.0, 10.0);
flipped.rotation = 180.0;
let mut items = vec![flipped];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
assert!(!items[0].is_strikeout);
}
#[test]
fn upside_down_run_is_underlined_by_a_rule_over_its_box() {
let mut flipped = item("Header", 100.0, 500.0, 10.0, 10.0);
flipped.rotation = 180.0;
let upright = item("Header", 100.0, 500.0, 10.0, 10.0);
let lines = vec![hline(98.0, 112.0, 511.5)];
let mut items = vec![flipped];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_underline);
assert!(!items[0].is_strikeout);
let mut items = vec![upright];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_underline);
assert!(!items[0].is_strikeout);
}
#[test]
fn oblique_run_is_never_decorated_by_a_horizontal_rule() {
let mut oblique = item("Callout", 100.0, 500.0, 40.0, 10.0);
oblique.rotation = 30.0;
oblique.height = 30.0;
let under = vec![hline(99.0, 141.0, 498.5)];
let mut items = vec![oblique.clone()];
mark_underlined_items(&mut items, &[], &under, 1);
assert!(!items[0].is_underline && !items[0].is_strikeout);
let through = vec![hline(100.0, 140.0, 503.0)];
let mut items = vec![oblique];
mark_underlined_items(&mut items, &[], &through, 1);
assert!(!items[0].is_underline && !items[0].is_strikeout);
let mut skewed = item("Header", 100.0, 500.0, 40.0, 10.0);
skewed.rotation = 2.0;
let mut items = vec![skewed];
mark_underlined_items(&mut items, &[], &under, 1);
assert!(items[0].is_underline);
}
#[test]
fn upside_down_fragments_of_different_sizes_share_a_strike_row() {
let mut small = item("Deleted", 100.0, 500.0, 40.0, 10.0);
small.rotation = 180.0;
let mut large = item("text", 145.0, 492.0, 40.0, 18.0);
large.rotation = 180.0;
let lines = vec![hline(100.0, 185.0, 507.0)];
let mut items = vec![small, large];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_strikeout, "{items:?}");
assert!(items[1].is_strikeout, "{items:?}");
}
#[test]
fn upside_down_run_is_struck_by_a_rule_through_its_glyphs() {
let mut flipped = item("Deleted", 100.0, 500.0, 40.0, 10.0);
flipped.rotation = 180.0;
let upright = item("Deleted", 100.0, 500.0, 40.0, 10.0);
let lines = vec![hline(100.0, 140.0, 507.0)];
let mut items = vec![flipped];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(items[0].is_strikeout);
assert!(!items[0].is_underline);
let mut items = vec![upright];
mark_underlined_items(&mut items, &[], &lines, 1);
assert!(!items[0].is_strikeout);
assert!(!items[0].is_underline);
}
}