use std::fmt;
use ahash::AHashMap;
use serde::{Deserialize, Serialize};
use super::document_structure::{ContentLayer, TextAnnotation};
use super::extraction::BoundingBox;
use super::metadata::Metadata;
use super::ocr_elements::{OcrBoundingGeometry, OcrConfidence, OcrElementLevel, OcrRotation};
use super::tables::Table;
use crate::types::ExtractedImage;
const SUPPRESS_IMAGE_OCR_RENDER_ATTRIBUTE: &str = "xberg:internal:suppress-image-ocr-render";
const LIST_ITEM_SOURCE_LABEL_ATTRIBUTE: &str = "list_marker";
const MEASURED_FONT_SIZE_ATTRIBUTE: &str = "xberg:internal:font-size-pt";
pub(crate) const NATIVE_TABLE_GRID_ATTRIBUTE: &str = "xberg:internal:native-table-grid";
#[cfg_attr(alef, alef(skip))]
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InternalElementId([u8; 15]);
impl Serialize for InternalElementId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for InternalElementId {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s.len() != 15 {
return Err(serde::de::Error::custom(format!(
"InternalElementId must be 15 bytes, got {}",
s.len()
)));
}
let mut buf = [0u8; 15];
buf.copy_from_slice(s.as_bytes());
Ok(Self(buf))
}
}
impl InternalElementId {
pub(crate) fn generate(kind_discriminant: &str, text: &str, page: Option<u32>, index: u32) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(kind_discriminant.as_bytes());
hasher.update(text.as_bytes());
hasher.update(&page.unwrap_or(u32::MAX).to_le_bytes());
hasher.update(&index.to_le_bytes());
let hash = hasher.finalize();
let bytes = &hash.as_bytes()[..6];
let mut buf = [0u8; 15];
buf[0] = b'i';
buf[1] = b'e';
buf[2] = b'-';
hex::encode_to_slice(bytes, &mut buf[3..]).expect("fixed size");
Self(buf)
}
pub(crate) fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap()
}
}
impl fmt::Display for InternalElementId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for InternalElementId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(all(any(feature = "ocr", feature = "ocr-pipeline"), feature = "pdf"))]
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct OcrPageCoordinateFrame {
pub page_number: u32,
pub width: u32,
pub height: u32,
pub unit: &'static str,
pub origin: &'static str,
}
#[cfg(all(any(feature = "ocr", feature = "ocr-pipeline"), feature = "pdf"))]
impl OcrPageCoordinateFrame {
pub fn new(page_number: u32, width: u32, height: u32) -> Self {
Self {
page_number,
width,
height,
unit: "pixel",
origin: "top_left",
}
}
}
#[cfg(feature = "pdf")]
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub(crate) struct PdfPageCoordinateFrame {
pub page_number: u32,
pub origin_x: f32,
pub origin_y: f32,
pub width: f32,
pub height: f32,
pub unit: &'static str,
pub origin: &'static str,
pub clockwise_rotation: i32,
}
#[cfg(feature = "pdf")]
impl PdfPageCoordinateFrame {
pub fn new(page_number: u32, llx: f32, lly: f32, urx: f32, ury: f32, clockwise_rotation: i32) -> Option<Self> {
let width = urx - llx;
let height = ury - lly;
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height <= 0.0 {
return None;
}
Some(Self {
page_number,
origin_x: llx,
origin_y: lly,
width,
height,
unit: "point",
origin: "bottom_left",
clockwise_rotation,
})
}
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InternalDocument {
pub elements: Vec<InternalElement>,
pub relationships: Vec<Relationship>,
pub source_format: String,
pub metadata: Metadata,
pub images: Vec<ExtractedImage>,
pub tables: Vec<Table>,
#[serde(default)]
pub diagrams: Vec<super::diagram::DiagramGraph>,
pub uris: Vec<super::uri::ExtractedUri>,
#[serde(skip)]
pub uris_dropped: usize,
pub children: Option<Vec<crate::types::ArchiveEntry>>,
pub mime_type: String,
pub processing_warnings: Vec<crate::types::ProcessingWarning>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ocr_page_failures: Vec<crate::types::OcrPageFailure>,
pub annotations: Option<Vec<crate::types::annotations::PdfAnnotation>>,
pub prebuilt_pages: Option<Vec<crate::types::PageContent>>,
pub pre_rendered_content: Option<String>,
pub prebuilt_ocr_elements: Option<Vec<crate::types::ocr_elements::OcrElement>>,
pub llm_usage: Option<Vec<crate::types::LlmUsage>>,
pub revisions: Option<Vec<crate::types::revisions::DocumentRevision>>,
pub form_fields: Vec<crate::types::PdfFormField>,
pub formulas: Vec<crate::types::Formula>,
#[serde(skip)]
pub recorded_formulas: Vec<crate::types::Formula>,
#[serde(skip)]
pub ocr_text_only: bool,
#[serde(skip)]
pub append_ocr_text: bool,
#[serde(skip)]
pub escape_markdown: bool,
#[serde(skip)]
pub include_watermarks: bool,
#[serde(skip)]
pub page_marker_format: Option<String>,
#[serde(skip)]
pub table_anchors: bool,
#[serde(skip)]
#[cfg(all(any(feature = "ocr", feature = "ocr-pipeline"), feature = "pdf"))]
pub ocr_coordinate_frame: Option<OcrPageCoordinateFrame>,
#[serde(skip)]
#[cfg(any(feature = "ocr", feature = "ocr-pipeline"))]
pub ocr_text_claimed_by_tables: bool,
}
impl From<crate::types::extraction::ExtractedDocument> for InternalDocument {
fn from(result: crate::types::extraction::ExtractedDocument) -> Self {
let extraction_method = result.extraction_method;
let mut doc = Self::new(result.mime_type.as_ref());
doc.mime_type = result.mime_type.into_owned();
doc.metadata = result.metadata;
if let Some(extraction_method) = extraction_method {
doc.metadata.additional.insert(
std::borrow::Cow::Borrowed("extraction_method"),
serde_json::Value::String(extraction_method.as_str().to_string()),
);
}
doc.tables = result.tables;
doc.images = result.images.unwrap_or_default();
doc.uris = result.uris.unwrap_or_default();
doc.children = result.children;
doc.annotations = result.annotations;
doc.processing_warnings = result.processing_warnings;
doc.ocr_page_failures = result.ocr_page_failures.unwrap_or_default();
doc.llm_usage = result.llm_usage;
doc.prebuilt_pages = result.pages;
doc.prebuilt_ocr_elements = result.ocr_elements;
doc.revisions = result.revisions;
doc.form_fields = result.form_fields;
doc.formulas = result.formulas;
doc.pre_rendered_content = if result.content.is_empty() {
None
} else {
Some(result.content)
};
doc
}
}
impl From<InternalDocument> for crate::types::extraction::ExtractedDocument {
fn from(doc: InternalDocument) -> Self {
crate::extraction::derive::derive_extraction_result(doc, false, crate::core::config::OutputFormat::Plain)
}
}
impl InternalDocument {
pub fn new(source_format: impl Into<String>) -> Self {
Self {
elements: Vec::new(),
relationships: Vec::new(),
source_format: source_format.into(),
metadata: Metadata::default(),
images: Vec::new(),
tables: Vec::new(),
diagrams: Vec::new(),
uris: Vec::new(),
uris_dropped: 0,
children: None,
mime_type: "application/octet-stream".to_string(),
processing_warnings: Vec::new(),
ocr_page_failures: Vec::new(),
annotations: None,
prebuilt_pages: None,
pre_rendered_content: None,
prebuilt_ocr_elements: None,
llm_usage: None,
revisions: None,
ocr_text_only: false,
append_ocr_text: false,
escape_markdown: true,
include_watermarks: false,
page_marker_format: None,
table_anchors: false,
form_fields: Vec::new(),
formulas: Vec::new(),
recorded_formulas: Vec::new(),
#[cfg(all(any(feature = "ocr", feature = "ocr-pipeline"), feature = "pdf"))]
ocr_coordinate_frame: None,
#[cfg(any(feature = "ocr", feature = "ocr-pipeline"))]
ocr_text_claimed_by_tables: false,
}
}
pub fn push_element(&mut self, element: InternalElement) -> u32 {
let idx = self.elements.len() as u32;
self.elements.push(element);
idx
}
pub fn push_relationship(&mut self, relationship: Relationship) {
self.relationships.push(relationship);
}
pub fn push_table(&mut self, table: Table) -> u32 {
let idx = self.tables.len() as u32;
self.tables.push(table);
idx
}
pub fn push_image(&mut self, image: ExtractedImage) -> u32 {
let idx = self.images.len() as u32;
self.images.push(image);
idx
}
pub(crate) const MAX_URIS: usize = 100_000;
pub fn push_uri(&mut self, uri: super::uri::ExtractedUri) {
if self.uris.len() < Self::MAX_URIS {
self.uris.push(uri);
} else {
self.uris_dropped += 1;
}
}
#[cfg(all(test, any(feature = "html", feature = "hwpx")))]
pub(crate) fn content(&self) -> String {
self.elements
.iter()
.map(|e| e.text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InternalElement {
pub id: InternalElementId,
pub kind: ElementKind,
pub text: String,
pub depth: u16,
pub page: Option<u32>,
pub bbox: Option<BoundingBox>,
pub layer: ContentLayer,
pub annotations: Vec<TextAnnotation>,
pub attributes: Option<AHashMap<String, String>>,
pub anchor: Option<String>,
pub ocr_geometry: Option<OcrBoundingGeometry>,
pub ocr_confidence: Option<OcrConfidence>,
pub ocr_rotation: Option<OcrRotation>,
}
impl InternalElement {
pub fn text(kind: ElementKind, text: impl Into<String>, depth: u16) -> Self {
let text = text.into();
let id = InternalElementId::generate(kind.discriminant(), &text, None, 0);
Self {
id,
kind,
text,
depth,
page: None,
bbox: None,
layer: ContentLayer::Body,
annotations: Vec::new(),
attributes: None,
anchor: None,
ocr_geometry: None,
ocr_confidence: None,
ocr_rotation: None,
}
}
#[cfg(any(
feature = "ocr",
feature = "office",
feature = "pdf",
paddle_ocr,
feature = "xml",
feature = "hwpx",
feature = "quality",
feature = "chunking",
test
))]
#[allow(dead_code)]
pub(crate) fn with_page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
#[cfg(feature = "office")]
pub(crate) fn with_bbox(mut self, bbox: BoundingBox) -> Self {
self.bbox = Some(bbox);
self
}
#[cfg(all(
test,
any(feature = "ocr", feature = "pdf", paddle_ocr, feature = "xml", feature = "office")
))]
pub(crate) fn with_layer(mut self, layer: ContentLayer) -> Self {
self.layer = layer;
self
}
#[cfg(test)]
pub(crate) fn with_anchor(mut self, anchor: impl Into<String>) -> Self {
self.anchor = Some(anchor.into());
self
}
#[cfg(any(feature = "xml", feature = "hwpx"))]
pub(crate) fn with_attributes(mut self, attributes: AHashMap<String, String>) -> Self {
self.attributes = Some(attributes);
self
}
#[cfg(any(
feature = "ocr",
feature = "xml",
feature = "archives",
feature = "hwpx",
// The only bare-`ocr-pipeline` caller lives in `extractors::pdf::ocr`, so gate on
// pdf+ocr-pipeline. `ocr-wasm` enables ocr-pipeline without pdf and has no caller. ~keep
all(feature = "pdf", feature = "ocr-pipeline")
))]
pub(crate) fn with_index(mut self, index: u32) -> Self {
self.id = InternalElementId::generate(self.kind.discriminant(), &self.text, self.page, index);
self
}
#[cfg(all(feature = "pdf", any(feature = "ocr", feature = "ocr-pipeline")))]
pub(crate) fn suppress_image_ocr_rendering(&mut self) {
self.attributes
.get_or_insert_with(AHashMap::new)
.insert(SUPPRESS_IMAGE_OCR_RENDER_ATTRIBUTE.to_string(), "true".to_string());
}
pub(crate) fn should_render_image_ocr(&self) -> bool {
!self
.attributes
.as_ref()
.is_some_and(|attributes| attributes.contains_key(SUPPRESS_IMAGE_OCR_RENDER_ATTRIBUTE))
}
pub(crate) fn set_list_item_source_label(&mut self, label: impl Into<String>) {
let label = label.into();
if label.is_empty() {
return;
}
self.attributes
.get_or_insert_with(AHashMap::new)
.insert(LIST_ITEM_SOURCE_LABEL_ATTRIBUTE.to_string(), label);
}
pub(crate) fn list_item_source_label(&self) -> Option<&str> {
self.attributes
.as_ref()?
.get(LIST_ITEM_SOURCE_LABEL_ATTRIBUTE)
.map(String::as_str)
}
pub(crate) fn public_attributes(&self) -> Option<std::collections::HashMap<String, String>> {
let original = self.attributes.as_ref()?;
let attributes: std::collections::HashMap<String, String> = original
.iter()
.filter(|(key, _)| !is_internal_only_attribute(key))
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
if attributes.is_empty() && !original.is_empty() {
None
} else {
Some(attributes)
}
}
#[cfg(feature = "pdf")]
pub(crate) fn set_measured_font_size(&mut self, font_size_pt: f32) {
if !font_size_pt.is_finite() || font_size_pt <= 0.0 {
return;
}
self.attributes
.get_or_insert_with(AHashMap::new)
.insert(MEASURED_FONT_SIZE_ATTRIBUTE.to_string(), font_size_pt.to_string());
}
#[cfg(feature = "pdf")]
pub(crate) fn measured_font_size(&self) -> Option<f32> {
let raw = self.attributes.as_ref()?.get(MEASURED_FONT_SIZE_ATTRIBUTE)?;
let value: f32 = raw.parse().ok()?;
(value.is_finite() && value > 0.0).then_some(value)
}
}
fn is_internal_only_attribute(key: &str) -> bool {
key == SUPPRESS_IMAGE_OCR_RENDER_ATTRIBUTE
|| key == MEASURED_FONT_SIZE_ATTRIBUTE
|| key == NATIVE_TABLE_GRID_ATTRIBUTE
}
pub(crate) fn list_item_source_label_from_attributes(attributes: Option<&AHashMap<String, String>>) -> Option<&str> {
attributes?.get(LIST_ITEM_SOURCE_LABEL_ATTRIBUTE).map(String::as_str)
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ElementKind {
Title,
Heading {
level: u8,
},
Paragraph,
ListItem {
ordered: bool,
},
Code,
Formula,
FootnoteDefinition,
FootnoteRef,
CommentDefinition,
CommentRef,
Citation,
Slide {
number: u32,
},
DefinitionTerm,
DefinitionDescription,
Admonition,
RawBlock,
MetadataBlock,
ListStart {
ordered: bool,
},
ListEnd,
QuoteStart,
QuoteEnd,
GroupStart,
GroupEnd,
Table {
table_index: u32,
},
Image {
image_index: u32,
},
PageBreak,
OcrText {
level: OcrElementLevel,
},
}
impl ElementKind {
pub(crate) fn discriminant(&self) -> &'static str {
match self {
Self::Title => "title",
Self::Heading { .. } => "heading",
Self::Paragraph => "paragraph",
Self::ListItem { .. } => "list_item",
Self::Code => "code",
Self::Formula => "formula",
Self::FootnoteDefinition => "footnote_definition",
Self::FootnoteRef => "footnote_ref",
Self::CommentDefinition => "comment_definition",
Self::CommentRef => "comment_ref",
Self::Citation => "citation",
Self::Slide { .. } => "slide",
Self::DefinitionTerm => "definition_term",
Self::DefinitionDescription => "definition_description",
Self::Admonition => "admonition",
Self::RawBlock => "raw_block",
Self::MetadataBlock => "metadata_block",
Self::ListStart { .. } => "list_start",
Self::ListEnd => "list_end",
Self::QuoteStart => "quote_start",
Self::QuoteEnd => "quote_end",
Self::GroupStart => "group_start",
Self::GroupEnd => "group_end",
Self::Table { .. } => "table",
Self::Image { .. } => "image",
Self::PageBreak => "page_break",
Self::OcrText { .. } => "ocr_text",
}
}
pub(crate) fn is_container_start(&self) -> bool {
matches!(self, Self::ListStart { .. } | Self::QuoteStart | Self::GroupStart)
}
pub(crate) fn is_container_end(&self) -> bool {
matches!(self, Self::ListEnd | Self::QuoteEnd | Self::GroupEnd)
}
#[cfg(test)]
pub(crate) fn matching_end(&self) -> Option<ElementKind> {
match self {
Self::ListStart { .. } => Some(Self::ListEnd),
Self::QuoteStart => Some(Self::QuoteEnd),
Self::GroupStart => Some(Self::GroupEnd),
_ => None,
}
}
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Relationship {
pub source: u32,
pub target: RelationshipTarget,
pub kind: RelationshipKind,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RelationshipTarget {
Index(u32),
Key(String),
}
pub use super::document_structure::RelationshipKind;
const _: () = {
#[allow(dead_code)]
fn assert_send_sync<T: Send + Sync>() {}
#[allow(dead_code)]
fn _check() {
assert_send_sync::<InternalDocument>();
assert_send_sync::<InternalElement>();
}
};
#[cfg(test)]
mod tests;