fix: exclude non-widget annotations from form flattening
This commit is contained in:
@@ -194,9 +194,10 @@ pub(crate) fn extract_pages_and_images(
|
||||
}
|
||||
|
||||
// PDFium's text API reads only the page content stream. Filled form
|
||||
// values commonly live in widget appearance streams, so flatten the
|
||||
// page's visible annotations in memory and reload before extracting text.
|
||||
// This does not initialize the form environment or execute document JS.
|
||||
// values commonly live in widget appearance streams, so promote only
|
||||
// those widget appearances into page content and reload before text
|
||||
// extraction. Non-widget annotations are excluded, and this does not
|
||||
// initialize the form environment or execute document JS.
|
||||
let extract_text = |page: &Page| -> Result<Vec<TextItem>, LiteParseError> {
|
||||
let text_page = page.text()?;
|
||||
extract_page_text_items(
|
||||
@@ -208,14 +209,15 @@ pub(crate) fn extract_pages_and_images(
|
||||
output_options.extract_text_metadata,
|
||||
)
|
||||
};
|
||||
let mut text_items = if page.has_form_widget_text() && page.flatten_for_display() {
|
||||
flattened_form_widgets = true;
|
||||
drop(page);
|
||||
let flattened_page = document.page(page_index)?;
|
||||
extract_text(&flattened_page)?
|
||||
} else {
|
||||
extract_text(&page)?
|
||||
};
|
||||
let mut text_items =
|
||||
if page.has_form_widget_text() && page.flatten_form_widgets_for_display() {
|
||||
flattened_form_widgets = true;
|
||||
drop(page);
|
||||
let flattened_page = document.page(page_index)?;
|
||||
extract_text(&flattened_page)?
|
||||
} else {
|
||||
extract_text(&page)?
|
||||
};
|
||||
assign_links(&mut text_items, &links);
|
||||
assign_strikethrough(&mut text_items, &graphics);
|
||||
|
||||
|
||||
@@ -321,6 +321,10 @@ async fn test_filled_acroform_values_are_extracted_as_text() {
|
||||
!parsed.text.contains("DEFAULT-ONLY-SHOULD-NOT-APPEAR"),
|
||||
"an unpainted default choice must not be treated as a filled value"
|
||||
);
|
||||
assert!(
|
||||
!parsed.text.contains("ANNOTATION-ONLY-SHOULD-NOT-APPEAR"),
|
||||
"non-widget annotation appearances must not become page text"
|
||||
);
|
||||
assert!(
|
||||
parsed.pages[0].form_fields.is_none(),
|
||||
"default text extraction must not enable structured form metadata"
|
||||
@@ -339,7 +343,19 @@ async fn test_filled_acroform_values_are_extracted_as_text() {
|
||||
|
||||
assert_eq!(
|
||||
with_metadata.pages[0].annotations.as_ref().unwrap().len(),
|
||||
5
|
||||
6
|
||||
);
|
||||
assert!(
|
||||
with_metadata.pages[0]
|
||||
.annotations
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|annotation| {
|
||||
annotation.subtype == "freetext"
|
||||
&& annotation.contents.as_deref() == Some("ANNOTATION-ONLY-SHOULD-NOT-APPEAR")
|
||||
}),
|
||||
"non-widget annotation metadata should remain available when requested"
|
||||
);
|
||||
let fields = with_metadata.pages[0].form_fields.as_ref().unwrap();
|
||||
assert_eq!(fields.len(), 5);
|
||||
|
||||
@@ -396,6 +396,7 @@ pub struct PdfiumBindings {
|
||||
unsafe extern "C" fn(FPDF_ANNOTATION, FPDF_BYTESTRING) -> FPDF_ANNOTATION,
|
||||
pub FPDFAnnot_GetObjNum: unsafe extern "C" fn(FPDF_ANNOTATION) -> std::os::raw::c_int,
|
||||
pub FPDFAnnot_GetFlags: unsafe extern "C" fn(FPDF_ANNOTATION) -> std::os::raw::c_int,
|
||||
pub FPDFAnnot_SetFlags: unsafe extern "C" fn(FPDF_ANNOTATION, std::os::raw::c_int) -> FPDF_BOOL,
|
||||
pub FPDFAnnot_GetObjectCount: unsafe extern "C" fn(FPDF_ANNOTATION) -> std::os::raw::c_int,
|
||||
pub FPDFAnnot_GetObject:
|
||||
unsafe extern "C" fn(FPDF_ANNOTATION, std::os::raw::c_int) -> FPDF_PAGEOBJECT,
|
||||
@@ -670,6 +671,7 @@ impl PdfiumBindings {
|
||||
FPDFAnnot_GetLinkedAnnot: load_fn!(lib, "FPDFAnnot_GetLinkedAnnot"),
|
||||
FPDFAnnot_GetObjNum: load_fn!(lib, "FPDFAnnot_GetObjNum"),
|
||||
FPDFAnnot_GetFlags: load_fn!(lib, "FPDFAnnot_GetFlags"),
|
||||
FPDFAnnot_SetFlags: load_fn!(lib, "FPDFAnnot_SetFlags"),
|
||||
FPDFAnnot_GetObjectCount: load_fn!(lib, "FPDFAnnot_GetObjectCount"),
|
||||
FPDFAnnot_GetObject: load_fn!(lib, "FPDFAnnot_GetObject"),
|
||||
FPDFAnnot_GetFormFieldFlags: load_fn!(lib, "FPDFAnnot_GetFormFieldFlags"),
|
||||
|
||||
@@ -939,15 +939,54 @@ impl<'doc, 'lib: 'doc> Page<'doc, 'lib> {
|
||||
false
|
||||
}
|
||||
|
||||
/// Promote visible annotation and form appearances into page content.
|
||||
/// Returns true only when PDFium changed the page.
|
||||
pub fn flatten_for_display(&self) -> bool {
|
||||
unsafe {
|
||||
/// Promote visible form-widget appearances into page content without
|
||||
/// admitting comment, markup, stamp, or other annotation appearances into
|
||||
/// the text layer.
|
||||
///
|
||||
/// PDFium's flatten operation is page-wide and otherwise consumes every
|
||||
/// visible annotation. Hide non-widget annotations in this disposable
|
||||
/// extraction document first; callers snapshot annotation metadata and
|
||||
/// reopen the pristine input for any later rendering work.
|
||||
///
|
||||
/// Returns true only when PDFium changed the page. If suppression or
|
||||
/// flattening fails, restores any changed flags and leaves extraction on
|
||||
/// the original page content.
|
||||
pub fn flatten_form_widgets_for_display(&self) -> bool {
|
||||
let mut suppressed = Vec::new();
|
||||
let count = unsafe { ffi!(FPDFPage_GetAnnotCount(self.handle)) };
|
||||
for index in 0..count {
|
||||
let annot = unsafe { ffi!(FPDFPage_GetAnnot(self.handle, index)) };
|
||||
if annot.is_null() {
|
||||
continue;
|
||||
}
|
||||
let subtype = unsafe { ffi!(FPDFAnnot_GetSubtype(annot)) };
|
||||
if subtype != pdfium_sys::FPDF_ANNOT_WIDGET as i32 {
|
||||
let flags = unsafe { ffi!(FPDFAnnot_GetFlags(annot)) };
|
||||
if flags & pdfium_sys::FPDF_ANNOT_FLAG_HIDDEN as i32 == 0 {
|
||||
let hidden_flags = flags | pdfium_sys::FPDF_ANNOT_FLAG_HIDDEN as i32;
|
||||
let changed = unsafe { ffi!(FPDFAnnot_SetFlags(annot, hidden_flags)) } != 0;
|
||||
unsafe { ffi!(FPDFPage_CloseAnnot(annot)) };
|
||||
if !changed {
|
||||
restore_annotation_flags(self.handle, &suppressed);
|
||||
return false;
|
||||
}
|
||||
suppressed.push((index, flags));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
unsafe { ffi!(FPDFPage_CloseAnnot(annot)) };
|
||||
}
|
||||
|
||||
let result = unsafe {
|
||||
ffi!(FPDFPage_Flatten(
|
||||
self.handle,
|
||||
pdfium_sys::FLAT_NORMALDISPLAY as i32
|
||||
)) == pdfium_sys::FLATTEN_SUCCESS as i32
|
||||
))
|
||||
};
|
||||
if result != pdfium_sys::FLATTEN_SUCCESS as i32 {
|
||||
restore_annotation_flags(self.handle, &suppressed);
|
||||
}
|
||||
result == pdfium_sys::FLATTEN_SUCCESS as i32
|
||||
}
|
||||
|
||||
/// Enumerate AcroForm widget annotations and resolve their field values
|
||||
@@ -1119,6 +1158,19 @@ fn annotation_paints_text(annot: pdfium_sys::FPDF_ANNOTATION) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn restore_annotation_flags(page: pdfium_sys::FPDF_PAGE, originals: &[(i32, i32)]) {
|
||||
for &(index, flags) in originals {
|
||||
let annot = unsafe { ffi!(FPDFPage_GetAnnot(page, index)) };
|
||||
if annot.is_null() {
|
||||
continue;
|
||||
}
|
||||
unsafe {
|
||||
ffi!(FPDFAnnot_SetFlags(annot, flags));
|
||||
ffi!(FPDFPage_CloseAnnot(annot));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn form_field_type_name(field_type: i32) -> &'static str {
|
||||
match field_type as u32 {
|
||||
pdfium_sys::FPDF_FORMFIELD_PUSHBUTTON => "pushbutton",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user