fix: exclude non-widget annotations from form flattening

This commit is contained in:
Neel Patel
2026-08-04 01:10:33 -04:00
parent 8ad92a3d02
commit d9ee62ec9d
5 changed files with 89 additions and 17 deletions
+13 -11
View File
@@ -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);
+17 -1
View File
@@ -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);
+2
View File
@@ -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"),
+57 -5
View File
@@ -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.