use std::collections::HashMap;
use crate::text_utils::should_join_items;
pub(crate) type PageExtraction = (Vec<TextItem>, Vec<PdfRect>, Vec<PdfLine>);
pub(crate) type FontEncodingMap = HashMap<u8, char>;
pub(crate) type PageFontEncodings = HashMap<String, FontEncodingMap>;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct FontWidthInfo {
pub(crate) widths: HashMap<u16, u16>,
pub(crate) default_width: u16,
pub(crate) space_width: u16,
pub(crate) is_cid: bool,
pub(crate) units_scale: f32,
pub(crate) wmode: u8,
}
pub(crate) type PageFontWidths = HashMap<String, FontWidthInfo>;
#[derive(Debug, Clone, Default)]
pub enum ItemType {
#[default]
Text,
Image,
Link(String),
FormField,
}
#[derive(Debug, Clone, Default)]
pub struct LayoutComplexity {
pub is_complex: bool,
pub pages_with_tables: Vec<u32>,
pub pages_with_columns: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct PdfLine {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
pub page: u32,
}
#[derive(Debug, Clone)]
pub struct PdfRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub page: u32,
}
#[derive(Debug, Clone)]
pub struct TextItem {
pub text: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub font: String,
pub font_size: f32,
pub page: u32,
pub is_bold: bool,
pub is_italic: bool,
pub is_underline: bool,
pub is_strikeout: bool,
pub item_type: ItemType,
pub mcid: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct TextLine {
pub items: Vec<TextItem>,
pub y: f32,
pub page: u32,
#[doc(hidden)]
pub adaptive_threshold: f32,
}
impl TextLine {
pub fn text(&self) -> String {
self.text_with_formatting(false, false, false)
}
pub fn text_with_formatting(
&self,
format_bold: bool,
format_italic: bool,
format_decorations: bool,
) -> String {
if !format_bold && !format_italic && !format_decorations {
return self.text_plain();
}
let single_char_threshold = self.adaptive_threshold;
let mut result = String::new();
let mut current_bold = false;
let mut current_italic = false;
let mut current_underline = false;
let mut current_strikeout = false;
for (i, item) in self.items.iter().enumerate() {
let text = item.text.as_str();
let text_trimmed = text.trim();
if text_trimmed.is_empty() {
continue;
}
let needs_space = if i == 0 || result.is_empty() {
false
} else {
let prev_item = &self.items[i - 1];
self.needs_space_between(prev_item, item, &result, single_char_threshold)
};
let has_leading_space = text.starts_with(' ');
let item_strikeout = format_decorations && item.is_strikeout;
let item_underline = format_decorations && item.is_underline && !item_strikeout;
let item_bold = format_bold && item.is_bold && !item_underline && !item_strikeout;
let item_italic = format_italic && item.is_italic && !item_underline && !item_strikeout;
if current_italic && !item_italic {
result.push('*');
current_italic = false;
}
if current_bold && !item_bold {
result.push_str("**");
current_bold = false;
}
if current_underline && !item_underline {
result.push_str("</u>");
current_underline = false;
}
if current_strikeout && !item_strikeout {
result.push_str("</s>");
current_strikeout = false;
}
if needs_space || (has_leading_space && !result.is_empty() && !result.ends_with(' ')) {
result.push(' ');
}
if item_underline && !current_underline {
result.push_str("<u>");
current_underline = true;
}
if item_strikeout && !current_strikeout {
result.push_str("<s>");
current_strikeout = true;
}
if item_bold && !current_bold {
result.push_str("**");
current_bold = true;
}
if item_italic && !current_italic {
result.push('*');
current_italic = true;
}
result.push_str(text_trimmed);
}
if current_italic {
result.push('*');
}
if current_bold {
result.push_str("**");
}
if current_underline {
result.push_str("</u>");
}
if current_strikeout {
result.push_str("</s>");
}
result
}
fn text_plain(&self) -> String {
let single_char_threshold = self.adaptive_threshold;
let mut result = String::new();
for (i, item) in self.items.iter().enumerate() {
let text = item.text.as_str();
if i == 0 {
result.push_str(text);
} else {
let prev_item = &self.items[i - 1];
if self.needs_space_between(prev_item, item, &result, single_char_threshold) {
result.push(' ');
}
result.push_str(text);
}
}
result
}
fn needs_space_between(
&self,
prev_item: &TextItem,
item: &TextItem,
result: &str,
single_char_threshold: f32,
) -> bool {
let text = item.text.as_str();
let prev_ends_with_hyphen = result.ends_with('-');
let curr_is_hyphen = text.trim() == "-";
let curr_starts_with_hyphen = text.starts_with('-');
let font_ratio = item.font_size / prev_item.font_size;
let reverse_font_ratio = prev_item.font_size / item.font_size;
let y_diff = (item.y - prev_item.y).abs();
let is_sub_super = font_ratio < 0.85 && y_diff > 1.0;
let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0;
let should_join = should_join_items(prev_item, item, single_char_threshold);
let prev_ends_with_space = result.ends_with(' ');
let curr_starts_with_space = text.starts_with(' ');
let space_already_exists = prev_ends_with_space || curr_starts_with_space;
!(prev_ends_with_hyphen
|| curr_is_hyphen
|| curr_starts_with_hyphen
|| is_sub_super
|| was_sub_super
|| should_join
|| space_already_exists)
}
}
#[cfg(test)]
mod formatting_tests {
use super::{ItemType, TextItem, TextLine};
fn item(text: &str, x: f32, width: f32, strikeout: bool) -> TextItem {
TextItem {
text: text.to_string(),
x,
y: 100.0,
width,
height: 12.0,
font: "F1".to_string(),
font_size: 12.0,
page: 1,
is_bold: false,
is_italic: false,
is_underline: false,
is_strikeout: strikeout,
item_type: ItemType::Text,
mcid: None,
}
}
fn line(items: Vec<TextItem>) -> TextLine {
TextLine {
items,
y: 100.0,
page: 1,
adaptive_threshold: 0.1,
}
}
#[test]
fn formatting_emits_semantic_strikeout() {
let line = line(vec![item("deleted", 10.0, 42.0, true)]);
assert_eq!(
line.text_with_formatting(true, true, true),
"<s>deleted</s>"
);
}
#[test]
fn formatting_closes_strikeout_before_live_text() {
let line = line(vec![
item("keep", 10.0, 24.0, false),
item("remove", 40.0, 42.0, true),
item("keep", 88.0, 24.0, false),
]);
assert_eq!(
line.text_with_formatting(true, true, true),
"keep <s>remove</s> keep"
);
}
#[test]
fn formatting_coalesces_adjacent_struck_items() {
let line = line(vec![
item("deleted", 10.0, 42.0, true),
item("words", 58.0, 30.0, true),
]);
assert_eq!(
line.text_with_formatting(true, true, true),
"<s>deleted words</s>"
);
}
#[test]
fn strikeout_takes_precedence_over_other_styles() {
let mut decorated = item("deleted", 10.0, 42.0, true);
decorated.is_bold = true;
decorated.is_italic = true;
decorated.is_underline = true;
let line = line(vec![decorated]);
assert_eq!(
line.text_with_formatting(true, true, true),
"<s>deleted</s>"
);
assert_eq!(line.text(), "deleted");
}
}