use super::KeywordPosition;
use easyofd_core::{OfdPage, ST_Box};
const POINT_PER_MM: f64 = 72.0 / 25.4;
#[derive(Debug, Clone)]
pub struct TextCodeEntry {
pub content: String,
pub x: Option<f64>,
pub y: Option<f64>,
pub delta_x: Vec<f64>,
pub delta_y: Vec<f64>,
pub page: usize,
pub boundary: ST_Box,
pub font_size: f64,
pub ctm: Option<[f64; 6]>,
}
impl TextCodeEntry {
#[must_use]
pub fn new(content: impl Into<String>, page: usize, boundary: ST_Box, font_size: f64) -> Self {
Self {
content: content.into(),
x: None,
y: None,
delta_x: Vec::new(),
delta_y: Vec::new(),
page,
boundary,
font_size,
ctm: None,
}
}
#[must_use]
pub fn coordinate(mut self, x: f64, y: f64) -> Self {
self.x = Some(x);
self.y = Some(y);
self
}
#[must_use]
pub fn delta_x(mut self, deltas: Vec<f64>) -> Self {
self.delta_x = deltas;
self
}
#[must_use]
pub fn delta_y(mut self, deltas: Vec<f64>) -> Self {
self.delta_y = deltas;
self
}
#[must_use]
pub fn ctm(mut self, ctm: [f64; 6]) -> Self {
self.ctm = Some(ctm);
self
}
}
fn char_to_byte_offset(s: &str, char_offset: usize) -> usize {
s.char_indices()
.nth(char_offset)
.map_or(s.len(), |(i, _)| i)
}
fn pad_delta(deltas: &[f64], content_len: usize) -> Vec<f64> {
if deltas.is_empty() || content_len == 0 {
return Vec::new();
}
if deltas.len() >= content_len {
return deltas.to_vec();
}
let mut result = deltas.to_vec();
let last = *result.last().expect("deltas 非空");
result.resize(content_len, last);
result
}
fn char_find_from(haystack: &[char], needle: &[char], from: usize) -> Option<usize> {
if needle.is_empty() || from > haystack.len() {
return None;
}
let end = haystack.len().saturating_sub(needle.len());
for i in from..=end {
if haystack[i..].starts_with(needle) {
return Some(i);
}
}
None
}
fn check_postfix_match(content_chars: &[char], keyword_chars: &[char]) -> Option<usize> {
if keyword_chars.is_empty() || content_chars.is_empty() {
return None;
}
let first_char = keyword_chars[0];
let start_index = content_chars.iter().rposition(|&c| c == first_char)?;
for (k, &ch) in content_chars[start_index..].iter().enumerate() {
if k >= keyword_chars.len() || ch != keyword_chars[k] {
return None;
}
}
Some(start_index)
}
fn search_next_text(
entries: &[TextCodeEntry],
start_index: usize,
keyword: &str,
first_match_content: &str,
) -> Vec<usize> {
let mut merge_indices = vec![start_index];
let mut merge_text = String::from(first_match_content);
let current_page = entries[start_index].page;
for j in (start_index + 1)..entries.len() {
let next = &entries[j];
if next.content.trim().is_empty() {
continue;
}
if next.page != current_page {
break;
}
merge_text.push_str(&next.content);
if merge_text == keyword || merge_text.starts_with(keyword) {
merge_indices.push(j);
break;
}
if keyword.starts_with(&merge_text) {
merge_indices.push(j);
} else {
break;
}
}
merge_indices
}
fn get_base_xy(
entry: &TextCodeEntry,
delta_x: &[f64],
delta_y: &[f64],
char_offset: usize,
) -> (f64, f64) {
let mut x = entry.boundary.top_left_x + entry.x.unwrap_or(0.0);
let mut y = entry.boundary.top_left_y + entry.y.unwrap_or(0.0);
for i in 0..char_offset {
if i < delta_x.len() {
x += delta_x[i];
}
if i < delta_y.len() {
y += delta_y[i];
}
}
(x, y)
}
fn get_string_width(start_char: usize, char_count: usize, delta_x: &[f64], font_size: f64) -> f64 {
if char_count == 0 {
return 0.0;
}
let mut width = font_size;
for i in start_char..(start_char + char_count - 1) {
if i < delta_x.len() {
width += delta_x[i];
}
}
width
}
fn merge_boxes(boxes: &[ST_Box]) -> ST_Box {
if boxes.is_empty() {
return ST_Box::new(0.0, 0.0, 0.0, 0.0);
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for b in boxes {
min_x = min_x.min(b.top_left_x);
min_y = min_y.min(b.top_left_y);
max_x = max_x.max(b.top_left_x + b.width);
max_y = max_y.max(b.top_left_y + b.height);
}
ST_Box::new(min_x, min_y, max_x - min_x, max_y - min_y)
}
fn ctm_transform(matrix: &[f64; 6], sx: f64, sy: f64) -> (f64, f64) {
let x = matrix[0] * sx + matrix[2] * sy + matrix[4];
let y = matrix[1] * sx + matrix[3] * sy + matrix[5];
(x, y)
}
fn merge_positions(positions: &[(f64, f64)]) -> ST_Box {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for &(px, py) in positions {
min_x = min_x.min(px);
min_y = min_y.min(py);
max_x = max_x.max(px);
max_y = max_y.max(py);
}
ST_Box::new(min_x, min_y, max_x - min_x, max_y - min_y)
}
fn compute_keyword_box(
entry: &TextCodeEntry,
content_chars: &[char],
text_index: usize,
keyword_len: usize,
) -> ST_Box {
let base_x = entry.x.unwrap_or(0.0);
let base_y = entry.y.unwrap_or(0.0);
let font_size = entry.font_size;
if keyword_len == 0 || content_chars.is_empty() || text_index >= content_chars.len() {
return ST_Box::new(
entry.boundary.top_left_x + base_x,
entry.boundary.top_left_y + base_y - font_size,
font_size,
font_size,
);
}
let delta_x = pad_delta(&entry.delta_x, content_chars.len());
let delta_y = pad_delta(&entry.delta_y, content_chars.len());
if let Some(matrix) = entry.ctm {
let mut x = base_x;
let mut y = base_y;
for i in 0..text_index {
if i < delta_x.len() {
x += delta_x[i];
}
if i < delta_y.len() {
y += delta_y[i];
}
}
let string_width = get_string_width(text_index, keyword_len, &delta_x, font_size);
let height = font_size;
let left_top = ctm_transform(&matrix, x, y - height);
let left_bottom = ctm_transform(&matrix, x, y);
let right_top = ctm_transform(&matrix, x + string_width, y - height);
let right_bottom = ctm_transform(&matrix, x + string_width, y);
let mut ctm_box = merge_positions(&[left_top, left_bottom, right_top, right_bottom]);
ctm_box.top_left_x += entry.boundary.top_left_x;
ctm_box.top_left_y += entry.boundary.top_left_y;
return ctm_box;
}
let mut x = entry.boundary.top_left_x + base_x;
let mut y = entry.boundary.top_left_y + base_y;
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
let end = text_index + keyword_len;
for i in 0..end {
if i >= text_index {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
if i < delta_x.len() {
x += delta_x[i];
}
if i < delta_y.len() {
y += delta_y[i];
}
}
let w = max_x - min_x + font_size;
let h = max_y - min_y + font_size;
ST_Box::new(min_x, min_y - font_size, w, h)
}
fn compute_merged_box(
entries: &[TextCodeEntry],
merge_indices: &[usize],
first_start_index: usize,
keyword_len: usize,
) -> ST_Box {
let mut boxes = Vec::new();
let mut total_length = 0;
for (idx, &entry_idx) in merge_indices.iter().enumerate() {
let entry = &entries[entry_idx];
let content_chars: Vec<char> = entry.content.chars().collect();
let content_len = content_chars.len();
let delta_x = pad_delta(&entry.delta_x, content_len);
let delta_y = pad_delta(&entry.delta_y, content_len);
let (text_length, start_char) = if idx == 0 && first_start_index > 0 {
let tl = content_len.saturating_sub(first_start_index);
total_length = tl;
(tl, first_start_index)
} else if total_length + content_len > keyword_len {
(keyword_len - total_length, 0)
} else {
total_length += content_len;
(content_len, 0)
};
let start_for_width = if idx == 0 && first_start_index > 0 {
first_start_index
} else {
0
};
let mut width = get_string_width(start_for_width, text_length, &delta_x, entry.font_size);
if width <= 0.0 {
width = entry.font_size;
}
let height = entry.font_size;
if let Some(matrix) = entry.ctm {
let mut x = entry.x.unwrap_or(0.0);
let mut y = entry.y.unwrap_or(0.0);
if idx == 0 && first_start_index > 0 {
for j in 0..first_start_index {
if j < delta_x.len() {
x += delta_x[j];
}
if j < delta_y.len() {
y += delta_y[j];
}
}
}
let left_bottom = ctm_transform(&matrix, x, y);
let right_top = ctm_transform(&matrix, x + width, y - height);
let mut ctm_box = merge_positions(&[left_bottom, right_top]);
ctm_box.top_left_x += entry.boundary.top_left_x;
ctm_box.top_left_y += entry.boundary.top_left_y;
boxes.push(ctm_box);
} else {
let (base_x, base_y) = get_base_xy(entry, &delta_x, &delta_y, start_char);
let box_ = ST_Box::new(base_x, base_y - height, width, height);
boxes.push(box_);
}
}
merge_boxes(&boxes)
}
#[derive(Debug)]
pub struct KeywordExtractor;
impl KeywordExtractor {
#[must_use]
pub fn get_keyword_positions(pages: &[OfdPage], keyword: &str) -> Vec<KeywordPosition> {
if keyword.is_empty() {
return Vec::new();
}
let mut positions = Vec::new();
for (page_idx, page) in pages.iter().enumerate() {
let page_num = page_idx + 1;
let page_positions = Self::search_page(page, page_num, keyword);
positions.extend(page_positions);
}
positions
}
#[allow(clippy::cast_precision_loss)]
fn search_page(page: &OfdPage, page_num: usize, keyword: &str) -> Vec<KeywordPosition> {
use easyofd_core::ContentObject;
let mut positions = Vec::new();
for obj in &page.content {
if let ContentObject::Text(text_obj) = obj {
let text = &text_obj.text;
let mut start = 0;
while let Some(idx) = text[start..].find(keyword) {
let match_start = start + idx;
let char_width = 3.0;
let x = text_obj.x + (match_start as f64) * char_width;
let y = text_obj.y;
let kw_width = (keyword.len() as f64) * char_width;
let kw_height = text_obj.size / POINT_PER_MM;
let rect = easyofd_core::ST_Box::new(x, y, kw_width, kw_height);
positions.push(KeywordPosition::new(page_num, rect).with_keyword(keyword));
start = match_start + keyword.len();
}
}
}
positions
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn get_keyword_positions_with_fonts(
pages: &[OfdPage],
keyword: &str,
font_sizes: &std::collections::HashMap<String, f64>,
) -> Vec<KeywordPosition> {
if keyword.is_empty() {
return Vec::new();
}
let mut positions = Vec::new();
for (page_idx, page) in pages.iter().enumerate() {
let page_num = page_idx + 1;
for obj in &page.content {
if let easyofd_core::ContentObject::Text(text_obj) = obj {
let text = &text_obj.text;
let char_width = font_sizes
.get(&text_obj.font)
.map_or(3.0, |size| size / POINT_PER_MM * 0.6);
let mut start = 0;
while let Some(idx) = text[start..].find(keyword) {
let match_start = start + idx;
let x = text_obj.x + (match_start as f64) * char_width;
let y = text_obj.y;
let kw_width = (keyword.len() as f64) * char_width;
let kw_height = text_obj.size / POINT_PER_MM;
let rect = easyofd_core::ST_Box::new(x, y, kw_width, kw_height);
positions.push(KeywordPosition::new(page_num, rect).with_keyword(keyword));
start = match_start + keyword.len();
}
}
}
}
positions
}
#[must_use]
pub fn get_keyword_positions_from_text_codes(
entries: &[TextCodeEntry],
keyword: &str,
) -> Vec<KeywordPosition> {
if keyword.is_empty() || entries.is_empty() {
return Vec::new();
}
let keyword_chars: Vec<char> = keyword.chars().collect();
let mut positions = Vec::new();
for i in 0..entries.len() {
let entry = &entries[i];
if entry.content.trim().is_empty() {
continue;
}
let content_chars: Vec<char> = entry.content.chars().collect();
if let Some(text_index) = char_find_from(&content_chars, &keyword_chars, 0) {
Self::add_normal_keyword_entry(
entry,
keyword,
&keyword_chars,
text_index,
&mut positions,
);
continue;
}
if keyword_chars.starts_with(&content_chars) && i != entries.len() - 1 {
Self::add_prefix_break(entries, i, keyword, &keyword_chars, &mut positions);
continue;
}
if let Some(start_index) = check_postfix_match(&content_chars, &keyword_chars) {
Self::add_postfix_break(
entries,
i,
start_index,
keyword,
&keyword_chars,
&mut positions,
);
}
}
positions
}
fn add_normal_keyword_entry(
entry: &TextCodeEntry,
keyword: &str,
keyword_chars: &[char],
first_text_index: usize,
positions: &mut Vec<KeywordPosition>,
) {
let content_chars: Vec<char> = entry.content.chars().collect();
let mut text_index = first_text_index;
loop {
let rect = compute_keyword_box(entry, &content_chars, text_index, keyword_chars.len());
positions.push(KeywordPosition::new(entry.page, rect).with_keyword(keyword));
let next_start = text_index + keyword_chars.len();
match char_find_from(&content_chars, keyword_chars, next_start) {
Some(next_idx) => text_index = next_idx,
None => break,
}
}
}
fn add_prefix_break(
entries: &[TextCodeEntry],
start_index: usize,
keyword: &str,
keyword_chars: &[char],
positions: &mut Vec<KeywordPosition>,
) {
let first_content = entries[start_index].content.clone();
let merge_indices = search_next_text(entries, start_index, keyword, &first_content);
let merged: String = merge_indices
.iter()
.map(|&idx| entries[idx].content.as_str())
.collect();
if merged.contains(keyword) {
let page = entries[start_index].page;
let rect = compute_merged_box(entries, &merge_indices, 0, keyword_chars.len());
positions.push(KeywordPosition::new(page, rect).with_keyword(keyword));
}
}
fn add_postfix_break(
entries: &[TextCodeEntry],
start_index: usize,
postfix_start: usize,
keyword: &str,
keyword_chars: &[char],
positions: &mut Vec<KeywordPosition>,
) {
let first_content = &entries[start_index].content;
let byte_start = char_to_byte_offset(first_content, postfix_start);
let first_match = &first_content[byte_start..];
let merge_indices = search_next_text(entries, start_index, keyword, first_match);
let merged: String = merge_indices
.iter()
.map(|&idx| entries[idx].content.as_str())
.collect();
if merged.contains(keyword) {
let page = entries[start_index].page;
let rect =
compute_merged_box(entries, &merge_indices, postfix_start, keyword_chars.len());
positions.push(KeywordPosition::new(page, rect).with_keyword(keyword));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use easyofd_core::TextObject;
fn make_page_with_text(text: &str) -> OfdPage {
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(TextObject::new(10.0, 20.0, text).size(10.0));
page
}
#[test]
fn test_keyword_extractor_single_match() {
let pages = vec![make_page_with_text("Hello World OFD Test")];
let positions = KeywordExtractor::get_keyword_positions(&pages, "OFD");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].page, 1);
assert_eq!(positions[0].keyword.as_deref(), Some("OFD"));
}
#[test]
fn test_keyword_extractor_multiple_matches() {
let pages = vec![make_page_with_text("OFD is an OFD format")];
let positions = KeywordExtractor::get_keyword_positions(&pages, "OFD");
assert_eq!(positions.len(), 2);
}
#[test]
fn test_keyword_extractor_no_match() {
let pages = vec![make_page_with_text("Hello World")];
let positions = KeywordExtractor::get_keyword_positions(&pages, "OFD");
assert!(positions.is_empty());
}
#[test]
fn test_keyword_extractor_empty_keyword() {
let pages = vec![make_page_with_text("Hello")];
let positions = KeywordExtractor::get_keyword_positions(&pages, "");
assert!(positions.is_empty());
}
#[test]
fn test_keyword_extractor_multiple_pages() {
let pages = vec![
make_page_with_text("Page 1"),
make_page_with_text("Page 2 OFD"),
];
let positions = KeywordExtractor::get_keyword_positions(&pages, "OFD");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].page, 2);
}
#[test]
fn test_keyword_extractor_with_fonts() {
let pages = vec![make_page_with_text("Test OFD keyword")];
let mut font_sizes = std::collections::HashMap::new();
font_sizes.insert("SimHei".to_string(), 12.0);
let positions =
KeywordExtractor::get_keyword_positions_with_fonts(&pages, "OFD", &font_sizes);
assert_eq!(positions.len(), 1);
}
#[test]
fn test_point_per_mm_constant() {
assert!((POINT_PER_MM - 2.8346).abs() < 0.01);
}
fn make_entry(content: &str, page: usize, x: f64, y: f64, font_size: f64) -> TextCodeEntry {
TextCodeEntry::new(
content,
page,
ST_Box::new(0.0, 0.0, 210.0, 297.0),
font_size,
)
.coordinate(x, y)
}
#[test]
fn test_single_text_code_match() {
let entries = vec![make_entry("Hello World OFD Test", 1, 10.0, 20.0, 3.0)];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "OFD");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].page, 1);
assert_eq!(positions[0].keyword.as_deref(), Some("OFD"));
}
#[test]
fn test_cross_2_text_codes() {
let entries = vec![
make_entry("电子", 1, 10.0, 20.0, 3.0),
make_entry("印章", 1, 30.0, 20.0, 3.0),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子印章");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].page, 1);
assert_eq!(positions[0].keyword.as_deref(), Some("电子印章"));
assert!(positions[0].rect.width > 3.0);
}
#[test]
fn test_cross_3_text_codes() {
let entries = vec![
make_entry("中华", 1, 10.0, 20.0, 3.0),
make_entry("人民", 1, 30.0, 20.0, 3.0),
make_entry("共和国", 1, 50.0, 20.0, 3.0),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "中华人民共和国");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("中华人民共和国"));
}
#[test]
fn test_with_delta_x() {
let entries = vec![
TextCodeEntry::new("电子印章", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.delta_x(vec![10.0, 10.0, 10.0]),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子印章");
assert_eq!(positions.len(), 1);
assert!((positions[0].rect.width - 33.0).abs() < 0.01);
}
#[test]
fn test_with_delta_x_cross_text_codes() {
let entries = vec![
TextCodeEntry::new("电子", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.delta_x(vec![10.0, 10.0]),
TextCodeEntry::new("印章", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(30.0, 20.0)
.delta_x(vec![10.0, 10.0]),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子印章");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("电子印章"));
assert!(positions[0].rect.width > 10.0);
}
#[test]
fn test_no_match() {
let entries = vec![make_entry("Hello World", 1, 10.0, 20.0, 3.0)];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子");
assert!(positions.is_empty());
}
#[test]
fn test_empty_keyword_from_entries() {
let entries = vec![make_entry("Hello", 1, 10.0, 20.0, 3.0)];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "");
assert!(positions.is_empty());
}
#[test]
fn test_empty_entries() {
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&[], "OFD");
assert!(positions.is_empty());
}
#[test]
fn test_postfix_match() {
let entries = vec![
make_entry("abc电", 1, 10.0, 20.0, 3.0),
make_entry("子印章", 1, 40.0, 20.0, 3.0),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("电子"));
}
#[test]
fn test_multiple_matches_same_entry() {
let entries = vec![make_entry("OFD是OFD格式", 1, 10.0, 20.0, 3.0)];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "OFD");
assert_eq!(positions.len(), 2);
}
#[test]
fn test_cross_page_boundary_no_match() {
let entries = vec![
make_entry("电子", 1, 10.0, 20.0, 3.0),
make_entry("印章", 2, 10.0, 20.0, 3.0),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子印章");
assert!(positions.is_empty());
}
#[test]
fn test_skip_empty_content() {
let entries = vec![
make_entry("电子", 1, 10.0, 20.0, 3.0),
make_entry(" ", 1, 20.0, 20.0, 3.0),
make_entry("印章", 1, 30.0, 20.0, 3.0),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子印章");
assert_eq!(positions.len(), 1);
}
#[test]
fn test_postfix_with_delta() {
let entries = vec![
TextCodeEntry::new("x电", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(5.0, 20.0)
.delta_x(vec![8.0, 8.0]),
TextCodeEntry::new("子", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(21.0, 20.0),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("电子"));
assert!((positions[0].rect.top_left_x - 13.0).abs() < 0.01);
}
#[test]
fn test_keyword_not_in_any_entry() {
let entries = vec![
make_entry("abc", 1, 10.0, 20.0, 3.0),
make_entry("def", 1, 30.0, 20.0, 3.0),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "xyz");
assert!(positions.is_empty());
}
#[test]
fn test_partial_prefix_no_completion() {
let entries = vec![
make_entry("电", 1, 10.0, 20.0, 3.0),
make_entry("xxx", 1, 30.0, 20.0, 3.0),
];
let positions =
KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子印章");
assert!(positions.is_empty());
}
#[test]
fn test_position_coordinates_basic() {
let entries = vec![
TextCodeEntry::new("AB", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.delta_x(vec![10.0, 10.0]),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "AB");
assert_eq!(positions.len(), 1);
assert!((positions[0].rect.width - 13.0).abs() < 0.01);
assert!((positions[0].rect.height - 3.0).abs() < 0.01);
assert!((positions[0].rect.top_left_x - 10.0).abs() < 0.01);
assert!((positions[0].rect.top_left_y - 17.0).abs() < 0.01);
}
#[test]
fn test_ctm_transform_identity() {
let matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
let (x, y) = ctm_transform(&matrix, 10.0, 20.0);
assert!((x - 10.0).abs() < f64::EPSILON);
assert!((y - 20.0).abs() < f64::EPSILON);
}
#[test]
fn test_ctm_transform_translation() {
let matrix = [1.0, 0.0, 0.0, 1.0, 5.0, 10.0];
let (x, y) = ctm_transform(&matrix, 0.0, 0.0);
assert!((x - 5.0).abs() < f64::EPSILON);
assert!((y - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_ctm_transform_90_rotation() {
let matrix = [0.0, 1.0, -1.0, 0.0, 0.0, 0.0];
let (x, y) = ctm_transform(&matrix, 3.0, 4.0);
assert!((x - (-4.0)).abs() < f64::EPSILON);
assert!((y - 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_ctm_keyword_single_match() {
let entries = vec![
TextCodeEntry::new("OFD", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.ctm([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "OFD");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("OFD"));
}
#[test]
fn test_ctm_keyword_90_rotation() {
let entries = vec![
TextCodeEntry::new("AB", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.ctm([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "AB");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("AB"));
assert!((positions[0].rect.width - 3.0).abs() < 0.01);
assert!((positions[0].rect.height - 3.0).abs() < 0.01);
}
#[test]
fn test_ctm_keyword_with_translation() {
let entries = vec![
TextCodeEntry::new("AB", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.ctm([1.0, 0.0, 0.0, 1.0, 50.0, 100.0]),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "AB");
assert_eq!(positions.len(), 1);
assert!((positions[0].rect.top_left_x - 60.0).abs() < 0.01);
assert!((positions[0].rect.top_left_y - 117.0).abs() < 0.01);
}
#[test]
fn test_ctm_keyword_cross_text_codes() {
let entries = vec![
TextCodeEntry::new("电", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(10.0, 20.0)
.ctm([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
TextCodeEntry::new("子", 1, ST_Box::new(0.0, 0.0, 210.0, 297.0), 3.0)
.coordinate(20.0, 20.0)
.ctm([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
];
let positions = KeywordExtractor::get_keyword_positions_from_text_codes(&entries, "电子");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].keyword.as_deref(), Some("电子"));
assert!(positions[0].rect.width > 3.0);
}
}