use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParserIdentity {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub adapter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub adapter_version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CoordinateOrigin {
TopLeft,
BottomLeft,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capabilities {
pub spans: bool,
pub char_offsets: bool,
pub tables: bool,
pub fingerprint: bool,
pub coordinate_origin: CoordinateOrigin,
pub crop_support: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PageGeometry {
pub id: String,
pub index: u32,
pub width: i64,
pub height: i64,
pub rotation: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroundingElement {
pub id: String,
pub page: String,
pub bbox: [i64; 4],
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroundingSpan {
pub id: String,
pub page: String,
pub bbox: [i64; 4],
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub element: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub char_start: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub char_end: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroundingCell {
pub row: u32,
pub col: u32,
pub row_span: u32,
pub col_span: u32,
pub bbox: [i64; 4],
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroundingTable {
pub id: String,
pub page: String,
pub bbox: [i64; 4],
pub cells: Vec<GroundingCell>,
}
pub trait GroundingSource {
fn parser(&self) -> ParserIdentity;
fn capabilities(&self) -> Capabilities;
fn fingerprint(&self) -> Option<String>;
fn pages(&self) -> Vec<PageGeometry>;
fn elements(&self) -> Vec<GroundingElement>;
fn spans(&self) -> Vec<GroundingSpan> {
Vec::new()
}
fn tables(&self) -> Vec<GroundingTable> {
Vec::new()
}
fn crop_ref(&self, _page: &str, _bbox: [i64; 4]) -> Option<String> {
None
}
fn element_by_id(&self, id: &str) -> Option<GroundingElement> {
self.elements().into_iter().find(|e| e.id == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Tiny;
impl GroundingSource for Tiny {
fn parser(&self) -> ParserIdentity {
ParserIdentity {
name: "tiny".into(),
version: "0.0.0".into(),
adapter: None,
adapter_version: None,
}
}
fn capabilities(&self) -> Capabilities {
Capabilities {
spans: false,
char_offsets: false,
tables: false,
fingerprint: false,
coordinate_origin: CoordinateOrigin::Unknown,
crop_support: false,
}
}
fn fingerprint(&self) -> Option<String> {
None
}
fn pages(&self) -> Vec<PageGeometry> {
vec![PageGeometry {
id: "p1".into(),
index: 1,
width: 10,
height: 10,
rotation: 0,
}]
}
fn elements(&self) -> Vec<GroundingElement> {
vec![GroundingElement {
id: "e1".into(),
page: "p1".into(),
bbox: [0, 0, 5, 5],
kind: "text_block".into(),
text: Some("hello".into()),
}]
}
}
#[test]
fn defaults_are_safe() {
let t = Tiny;
assert!(t.spans().is_empty());
assert!(t.tables().is_empty());
assert_eq!(
t.element_by_id("e1").unwrap().text.as_deref(),
Some("hello")
);
assert!(t.element_by_id("nope").is_none());
assert!(t.crop_ref("p1", [0, 0, 5, 5]).is_none());
}
}