use serde::Serialize;
use std::collections::{BTreeMap, HashMap};
#[doc(hidden)]
#[derive(Debug, Clone)]
pub enum PdfInput {
Path(String),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct TextItem {
pub text: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub rotation: f32,
pub font_name: Option<String>,
pub font_size: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_height: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_ascent: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_descent: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_weight: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_flags: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_width: Option<f32>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub font_is_buggy: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub has_unicode_map_error: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcid: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fill_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stroke_color: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub char_codes: Vec<u32>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub trailing_space_generated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub strike: bool,
#[serde(skip)]
pub words: Vec<WordBox>,
}
pub struct TextMetadata<'a> {
pub font_height: Option<f32>,
pub font_ascent: Option<f32>,
pub font_descent: Option<f32>,
pub font_weight: Option<i32>,
pub text_width: Option<f32>,
pub font_is_buggy: Option<bool>,
pub mcid: Option<i32>,
pub fill_color: Option<&'a str>,
pub stroke_color: Option<&'a str>,
pub char_codes: Option<&'a [u32]>,
pub trailing_space_generated: Option<bool>,
}
impl TextItem {
pub fn text_metadata(&self, enabled: bool) -> TextMetadata<'_> {
if enabled {
TextMetadata {
font_height: self.font_height,
font_ascent: self.font_ascent,
font_descent: self.font_descent,
font_weight: self.font_weight,
text_width: self.text_width,
font_is_buggy: Some(self.font_is_buggy),
mcid: self.mcid,
fill_color: self.fill_color.as_deref(),
stroke_color: self.stroke_color.as_deref(),
char_codes: Some(&self.char_codes),
trailing_space_generated: Some(self.trailing_space_generated),
}
} else {
TextMetadata {
font_height: None,
font_ascent: None,
font_descent: None,
font_weight: None,
text_width: None,
font_is_buggy: None,
mcid: None,
fill_color: None,
stroke_color: None,
char_codes: None,
trailing_space_generated: None,
}
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct WordBox {
pub text: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[doc(hidden)]
#[derive(Debug, Serialize)]
pub struct Page {
pub page_number: usize,
pub page_width: f32,
pub page_height: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_bounds: Option<Rect>,
pub text_items: Vec<TextItem>,
#[serde(skip)]
pub graphics: Vec<GraphicPrimitive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vector_graphics: Option<VectorGraphics>,
#[serde(skip)]
pub struct_nodes: Vec<StructNode>,
#[serde(skip)]
pub image_refs: Vec<ImageRef>,
#[serde(skip)]
pub annotations: Option<Vec<DocumentAnnotation>>,
#[serde(skip)]
pub form_fields: Option<Vec<FormField>>,
#[serde(skip)]
pub structure_tree: Option<StructureTree>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentAnnotation {
pub subtype: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contents: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rect: Option<Rect>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub quadpoint_rects: Vec<Rect>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(untagged)]
pub enum StructureAttributeValue {
Boolean(bool),
Number(f32),
String(String),
}
#[derive(Debug, Clone, Serialize)]
pub struct StructureTree {
pub roots: Vec<StructureTreeElement>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StructureTreeElement {
#[serde(rename = "type")]
pub element_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actual_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alt_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, StructureAttributeValue>,
pub marked_content_ids: Vec<i32>,
pub children: Vec<StructureTreeElement>,
pub annotations: Vec<DocumentAnnotation>,
}
#[derive(Debug, Clone, Serialize)]
pub struct FormField {
pub id: String,
#[serde(rename = "type")]
pub field_type: String,
pub page: u32,
pub annotation_index: i32,
pub widget_index: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_number: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alternate_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub export_value: Option<String>,
pub field_flags: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_index: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checked: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rect: Option<Rect>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub selected_options: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct XfaPacket {
pub index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub content_length: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ScreenshotRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub color: String,
pub is_line: bool,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct OutlineTarget {
pub level: u8,
pub title: String,
pub page_index: i32,
pub y_pdf: Option<f32>,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct StructNode {
pub role: String,
pub mcids: Vec<i32>,
pub bbox: Option<Rect>,
pub alt_text: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ParsedPage {
pub page_number: usize,
pub page_width: f32,
pub page_height: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_bounds: Option<Rect>,
pub text: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub markdown: String,
pub text_items: Vec<TextItem>,
#[serde(skip)]
pub projected_lines: Vec<ProjectedLine>,
#[serde(skip)]
pub regions: Region,
#[serde(skip)]
pub graphics: Vec<GraphicPrimitive>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vector_graphics: Option<VectorGraphics>,
#[serde(skip)]
pub figures: Vec<Rect>,
#[serde(skip)]
pub struct_nodes: Vec<StructNode>,
#[serde(skip)]
pub image_refs: Vec<ImageRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity: Option<crate::ocr_merge::PageComplexityStats>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Vec<DocumentAnnotation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub form_fields: Option<Vec<FormField>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub structure_tree: Option<StructureTree>,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct ImageRef {
pub id: String,
pub bbox: Rect,
pub obj_index: usize,
pub format: String,
pub pixel_width: u32,
pub pixel_height: u32,
pub rotation: f32,
pub jpeg_bytes: Option<Vec<u8>>,
pub raw_bytes: Option<Vec<u8>>,
pub bits_per_pixel: u32,
pub colorspace: i32,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExtractedImage {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
pub page: u32,
pub bbox: Rect,
pub width: u32,
pub height: u32,
pub rotation: f32,
pub format: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub duplicate_of: Option<String>,
#[serde(skip)]
pub bytes: std::sync::Arc<Vec<u8>>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Default, Serialize)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct VectorGraphics {
pub shapes: Vec<VectorShape>,
pub lines: Vec<VectorLine>,
}
#[derive(Debug, Clone, Serialize)]
pub struct VectorShape {
pub bbox: Rect,
pub stroke: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stroke_color: Option<String>,
pub fill: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub fill_color: Option<String>,
pub has_curve: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct VectorLine {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
pub stroke: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stroke_width: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stroke_color: Option<String>,
pub fill: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub fill_color: Option<String>,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub enum GraphicPrimitive {
Stroke {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
color: Option<String>,
width: f32,
},
Rect {
bbox: Rect,
fill: Option<String>,
stroke: Option<String>,
},
}
impl GraphicPrimitive {
pub fn bbox(&self) -> Rect {
match self {
GraphicPrimitive::Stroke { x1, y1, x2, y2, .. } => {
let x = x1.min(*x2);
let y = y1.min(*y2);
Rect {
x,
y,
width: (x2 - x1).abs(),
height: (y2 - y1).abs(),
}
}
GraphicPrimitive::Rect { bbox, .. } => bbox.clone(),
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Serialize)]
pub struct ProjectedLine {
pub text: String,
pub bbox: Rect,
pub anchor: Anchor,
pub indent_x: f32,
pub dominant_font_size: f32,
pub font_size_is_estimated: bool,
pub heading_font_size: Option<f32>,
pub dominant_font_name: Option<String>,
pub all_bold: bool,
pub all_italic: bool,
pub all_mono: bool,
pub all_strike: bool,
pub spans: Vec<TextItem>,
pub region_path: Vec<u16>,
pub mcid: Option<i32>,
pub in_figure: bool,
}
#[doc(hidden)]
#[derive(Debug, Clone, Default)]
pub struct Region {
pub bbox: Rect,
pub kind: RegionKind,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub enum RegionKind {
Leaf {
item_indices: Vec<usize>,
},
Split {
axis: CutAxis,
children: Vec<Region>,
},
}
impl Default for RegionKind {
fn default() -> Self {
RegionKind::Leaf {
item_indices: Vec::new(),
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CutAxis {
Horizontal,
Vertical,
}
#[doc(hidden)]
#[derive(Debug, Serialize)]
pub enum Snap {
Left,
Right,
Center,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum Anchor {
Left,
Right,
Center,
Floating,
}
#[doc(hidden)]
#[derive(Debug, Serialize)]
pub struct ProjectedTextItem {
pub item: TextItem,
pub snap: Snap,
pub anchor: Anchor,
pub is_dup: bool,
pub rendered: bool,
pub num_spaces: usize,
pub force_unsnapped: bool,
pub is_margin_line_number: bool,
pub rotated: bool,
pub d: f32,
pub orig_x: f32,
pub orig_y: f32,
pub orig_width: f32,
pub orig_height: f32,
pub orig_rotation: f32,
}
#[doc(hidden)]
pub type AnchorMap = HashMap<i32, Vec<(usize, usize)>>;
#[cfg(test)]
mod tests {
use super::*;
fn sample_item() -> TextItem {
TextItem {
text: "hi".into(),
x: 1.0,
y: 2.0,
width: 10.0,
height: 4.0,
font_name: Some("Arial".into()),
font_size: Some(12.0),
..Default::default()
}
}
#[test]
fn text_item_skips_none_fields() {
let item = sample_item();
let s = serde_json::to_string(&item).unwrap();
assert!(!s.contains("font_height"));
assert!(!s.contains("confidence"));
assert!(!s.contains("font_is_buggy"));
assert!(s.contains("\"text\":\"hi\""));
}
#[test]
fn text_item_includes_buggy_flag_when_true() {
let mut item = sample_item();
item.font_is_buggy = true;
let s = serde_json::to_string(&item).unwrap();
assert!(s.contains("font_is_buggy"));
}
#[test]
fn page_serializes() {
let p = Page {
page_number: 1,
page_width: 100.0,
page_height: 200.0,
content_bounds: None,
text_items: vec![sample_item()],
graphics: vec![],
vector_graphics: None,
struct_nodes: vec![],
image_refs: vec![],
annotations: None,
form_fields: None,
structure_tree: None,
};
let s = serde_json::to_string(&p).unwrap();
assert!(s.contains("\"page_number\":1"));
}
#[test]
fn anchor_map_basic() {
let mut m: AnchorMap = HashMap::new();
m.entry(5).or_default().push((1, 2));
assert_eq!(m.get(&5).unwrap()[0], (1, 2));
}
}