Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.6.13

### Fixes
- **Render filled PDF form fields**: `convert_pdf_to_image` now calls `init_forms()` so AcroForm/XFA field values (text typed into fillable fields) are painted into the rendered page image instead of being silently dropped.

## 1.6.12

### Fixes
Expand Down
Binary file added sample-docs/form-field.pdf
Binary file not shown.
52 changes: 52 additions & 0 deletions test_unstructured_inference/inference/test_pdf_image_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import numpy as np
import pypdfium2 as pdfium

from unstructured_inference.inference import pdf_image

# sample-docs/form-field.pdf is a 1-page PDF with an empty content stream and a single
# filled text form field. The field value ("FORMVALUE777") is drawn only by the widget
# annotation's appearance stream, so it renders only when the form-fill environment is
# initialized (init_forms). Geometry below mirrors the fixture's widget rectangle.
FORM_PDF = "sample-docs/form-field.pdf"

@cubic-dev-ai cubic-dev-ai Bot Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new tests point at a sparse-checkout placeholder instead of a real PDF fixture, so rendering will fail before exercising init_forms().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test_unstructured_inference/inference/test_pdf_image_forms.py, line 12:

<comment>The new tests point at a sparse-checkout placeholder instead of a real PDF fixture, so rendering will fail before exercising `init_forms()`.</comment>

<file context>
@@ -2,84 +2,19 @@
+# filled text form field. The field value ("FORMVALUE777") is drawn only by the widget
+# annotation's appearance stream, so it renders only when the form-fill environment is
+# initialized (init_forms). Geometry below mirrors the fixture's widget rectangle.
+FORM_PDF = "sample-docs/form-field.pdf"
 PAGE_WIDTH, PAGE_HEIGHT = 612, 792
-# Widget rectangle in PDF user space (origin bottom-left): x1, y1, x2, y2.
</file context>
Fix with cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fixture is a real pdf file committed in 3e82abe

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for the correction.

PAGE_WIDTH, PAGE_HEIGHT = 612, 792
FIELD_RECT = (40, 700, 320, 724) # x1, y1, x2, y2 in PDF user space (origin bottom-left)
RENDER_DPI = 200


def _field_region_dark_pixels(img) -> int:
"""Count dark pixels inside the form field's rectangle in the rendered image."""
gray = img.convert("L")
scale_x = gray.width / PAGE_WIDTH
scale_y = gray.height / PAGE_HEIGHT
x0, y0, x1, y1 = FIELD_RECT
# PDF user space is bottom-up; image space is top-down.
box = (
int(x0 * scale_x),
int((PAGE_HEIGHT - y1) * scale_y),
int(x1 * scale_x),
int((PAGE_HEIGHT - y0) * scale_y),
)
crop = np.array(gray.crop(box))
return int(np.count_nonzero(crop < 128))


def test_convert_pdf_to_image_renders_acroform_field_value():
"""Filled form-field values are painted into the rendered page image."""
img = pdf_image.convert_pdf_to_image(filename=FORM_PDF, dpi=RENDER_DPI)[0]

assert _field_region_dark_pixels(img) > 100, "Expected the form field value to be rendered"


def test_convert_pdf_to_image_drops_form_field_without_init_forms(monkeypatch):
"""Control: without init_forms() the widget appearance is not painted.

Patching init_forms() to a no-op reproduces the pre-fix behavior and proves the
rendered field value in the test above comes specifically from initializing the
form-fill environment, not from the page content stream (which is empty here).
"""
monkeypatch.setattr(pdfium.PdfDocument, "init_forms", lambda self, *a, **k: None)
img = pdf_image.convert_pdf_to_image(filename=FORM_PDF, dpi=RENDER_DPI)[0]

assert _field_region_dark_pixels(img) == 0, "Field should be blank without form init"
2 changes: 1 addition & 1 deletion unstructured_inference/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.6.12" # pragma: no cover
__version__ = "1.6.13" # pragma: no cover
8 changes: 8 additions & 0 deletions unstructured_inference/inference/pdf_image.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
import math
import os
from functools import lru_cache
Expand Down Expand Up @@ -166,6 +167,13 @@ def _in_range(page_num: int) -> bool:

with _pdfium_lock:
pdf = pdfium.PdfDocument(filename or file, password=password)
# Initialize the form-fill environment so AcroForm/XFA field values
# (e.g. text typed into fillable fields) are painted into the rendered
# image. Without this, pdfium silently drops widget annotation content
# even though may_draw_forms defaults to True on page.render().
# Fall back to page rendering without form appearances when form env init fails.
with contextlib.suppress(pdfium.PdfiumError):
pdf.init_forms()
n_pages = len(pdf)

# Pre-scan page rotations so the (heavier) text-orientation pass only runs on the
Expand Down
Loading