use super::*;
use crate::parser::filters::FilterRecovery;
use crate::parser::ParseError;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RecoveryLocation {
PageContents(usize),
FormXObject { name: String, object: (u32, u16) },
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TextRecoveryAction {
Recovered(FilterRecovery),
Omitted { error: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextRecoveryDiagnostic {
pub location: RecoveryLocation,
pub action: TextRecoveryAction,
}
#[derive(Debug)]
pub struct RecoveredText {
pub page_index: u32,
pub text: ExtractedText,
pub diagnostics: Vec<TextRecoveryDiagnostic>,
}
pub(super) struct RecoveryContext {
pub(super) max_stream_bytes: usize,
pub(super) diagnostics: Vec<TextRecoveryDiagnostic>,
}
impl TextExtractor {
pub fn extract_from_page_with_recovery<R: Read + Seek>(
&mut self,
document: &PdfDocument<R>,
page_index: u32,
max_stream_bytes: usize,
) -> ParseResult<RecoveredText> {
self.recovery = Some(RecoveryContext {
max_stream_bytes,
diagnostics: Vec::new(),
});
let result = self.extract_page_impl(document, page_index);
let context = self
.recovery
.take()
.expect("recovery context exists for this call");
result.map(|text| RecoveredText {
page_index,
text,
diagnostics: context.diagnostics,
})
}
}
impl TextExtractor {
pub(super) fn decode_content_for_recovery<R: Read + Seek>(
&mut self,
document: &PdfDocument<R>,
stream: &crate::parser::PdfStream,
location: RecoveryLocation,
) -> ParseResult<Option<Vec<u8>>> {
use crate::parser::filters::StreamRecoveryErrorKind;
let Some(context) = self.recovery.as_mut() else {
return document.decode_stream(stream).map(Some);
};
match document.decode_stream_with_recovery(stream, context.max_stream_bytes) {
Ok(decoded) => {
context
.diagnostics
.extend(decoded.diagnostics.into_iter().map(|diagnostic| {
TextRecoveryDiagnostic {
location: location.clone(),
action: TextRecoveryAction::Recovered(diagnostic),
}
}));
Ok(Some(decoded.data))
}
Err(error) if error.kind == StreamRecoveryErrorKind::InvalidFlate => {
context.diagnostics.push(TextRecoveryDiagnostic {
location,
action: TextRecoveryAction::Omitted {
error: error.source.to_string(),
},
});
Ok(None)
}
Err(error) => Err(error.source),
}
}
pub(super) fn recovery_content_groups<R: Read + Seek>(
&mut self,
document: &PdfDocument<R>,
page: &crate::parser::ParsedPage,
) -> ParseResult<Vec<Vec<Vec<u8>>>> {
use crate::parser::PdfObject;
let Some(contents) = page.dict.get("Contents") else {
return Ok(Vec::new());
};
let resolved = document.resolve(contents)?;
let items = match resolved {
PdfObject::Stream(s) => vec![PdfObject::Stream(s)],
PdfObject::Array(a) => a.0,
_ => {
return Err(ParseError::SyntaxError {
position: 0,
message: "Contents must be a stream or array of streams".into(),
})
}
};
let mut groups = Vec::new();
let mut current = Vec::new();
for (index, item) in items.into_iter().enumerate() {
let resolved = document.resolve(&item)?;
if let PdfObject::Stream(stream) = resolved {
let before = self.recovery.as_ref().unwrap().diagnostics.len();
let data = self.decode_content_for_recovery(
document,
&stream,
RecoveryLocation::PageContents(index),
)?;
if self.recovery.as_ref().unwrap().diagnostics.len() != before {
if !current.is_empty() {
groups.push(std::mem::take(&mut current));
}
if let Some(data) = data {
groups.push(vec![data]);
}
} else if let Some(data) = data {
current.push(data);
}
}
}
if !current.is_empty() {
groups.push(current);
}
Ok(groups)
}
}