Skip to content

CP-14715 - Fix auto-detected form fields on rotated PDFs - #84

Merged
bernardodsanderson merged 2 commits into
mainfrom
CP-14715
Aug 4, 2026
Merged

CP-14715 - Fix auto-detected form fields on rotated PDFs#84
bernardodsanderson merged 2 commits into
mainfrom
CP-14715

Conversation

@bernardodsanderson

Copy link
Copy Markdown
Collaborator

What

When a PDF page is stored with a rotation flag, auto-detected fillable fields now line up with the on-page widgets in the template editor, and filled values land in the correct boxes when the document is completed. Upright pages are unchanged.

Why

Auto-detect placed fields in the PDF's unrotated coordinate space while the editor and signing output use the rotated visual layout, so fields and values appeared offset on rotated pages.

How to test

  1. Create a DocuSeal paperwork task in ATS and open the template editor iframe.
  2. Upload a fillable PDF with a rotated page (e.g. docuseal/tmp/paper_audit/fixtures/acro-rot90.pdf).
  3. Confirm auto-detected field boxes overlay the on-page widgets (not floating elsewhere).
  4. Optionally complete a signing and confirm values sit inside the boxes.
  5. Upload an upright fillable PDF (e.g. acro-letter.pdf) and confirm fields still align.
  6. Or run: bundle exec rspec spec/lib/templates/find_acro_fields_spec.rb

What
When a PDF page is stored with a rotation flag, auto-detected fillable fields now line up with the on-page widgets in the template editor, and filled values land in the correct boxes when the document is completed. Upright pages are unchanged.

Why
Auto-detect placed fields in the PDF's unrotated coordinate space while the editor and signing output use the rotated visual layout, so fields and values appeared offset on rotated pages.

How to test
1. Create a DocuSeal paperwork task in ATS and open the template editor iframe.
2. Upload a fillable PDF with a rotated page (e.g. docuseal/tmp/paper_audit/fixtures/acro-rot90.pdf).
3. Confirm auto-detected field boxes overlay the on-page widgets (not floating elsewhere).
4. Optionally complete a signing and confirm values sit inside the boxes.
5. Upload an upright fillable PDF (e.g. acro-letter.pdf) and confirm fields still align.
6. Or run: bundle exec rspec spec/lib/templates/find_acro_fields_spec.rb

@spaulsandhu spaulsandhu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 This is a semi-automated review done by Paul, with assistance from a review agent. Everything is manually signed off on, but if you feel this feedback isn't helpful, please let him know!

I checked the corner math against hexapdf 1.0.3's Page#rotate rather than taking the comment's word for it, and it lines up exactly. Their flatten matrices evaluate to (y, W - x) for 90, (W - x, H - y) for 180 and (H - y, x) for 270, which is precisely what apply_page_rotation does. The premise holds up too: generate_result_attachments.rb really does page.rotate(0, flatten: true) before it reads page.box.width, and previews come off Pdfium, so visual space is the right target for all three surfaces. 👍🏽

Two other things I went looking for and was happy not to find: page[:Rotate] picks up rotation inherited from the /Pages node (hexapdf walks the tree for its inheritable fields), and % 360 means a stored -90 lands on 270 rather than falling through. Worked example per rotation in the spec comments made this a genuinely quick read.

Approving. None of the below needs to gate it: a coverage ask, one thing the fix doesn't quite reach, and a question about where this patch lives long term.

Process question rather than a code one: git log on lib/templates/find_acro_fields.rb is upstream commits the whole way down ("fix form cells", "fix standalone radio"), so as far as I can tell this would be our first fork-local change to that file. Which means a conflict to hand-resolve on every sync from docusealco/docuseal from here on.

Given the fix is self-contained and not CareerPlug-specific at all, is it worth opening it upstream too? Even if they sit on it, having the PR filed makes the eventual merge somebody else's problem instead of ours.

Not asking you to hold this one up for that, ship it either way.

—Paul-bot


def build_pdf(rotate: nil)
doc = HexaPDF::Document.new
page = doc.pages.add([0, 0, page_w, page_h])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Every case here uses a [0, 0, page_w, page_h] box, which leaves the one assumption in the new math untested.

apply_page_rotation takes width and height as extents, while hexapdf's own flatten uses the absolute box edges (pbox.bottom, pbox.right, pbox.top, pbox.left). Those two only agree because correct_coordinates has already shifted the Rect onto a 0-origin box before we get here. Nothing in the spec would catch it if that ordering ever changed.

Can we get one case with an offset MediaBox, say [10, 20, 622, 812] at /Rotate 90? A CropBox that differs from the MediaBox would be the other one worth having, since media_box up top is actually the CropBox whenever there is one.

rotation = (page[:Rotate] || 0).to_i % 360
if rotation != 0
x0, y0, x1, y1 = apply_page_rotation(x0, y0, x1, y1, rotation, page_width, page_height)
page_width, page_height = page_height, page_width if SWAP_DIMENSION_ROTATIONS.include?(rotation)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing the swap doesn't reach, worth naming so it doesn't look covered: cell_w further down at line 91.

After a 90 or 270, w is the visual width, which for a comb field whose cells ran along media-x is now the short axis. So w / page_width / MaxLen divides the wrong side, and comb text on a rotated page still gets cells in the wrong place.

Niche enough that I wouldn't grow this PR for it, and it was equally broken before. But the description says fields line up, so I'd either carve out the comb case or drop a note here.


# Map a media-space Rect through PDF /Rotate (clockwise degrees) into visual
# page space. Mirrors HexaPDF::Type::Page#rotate(0, flatten: true) corner math.
def apply_page_rotation(llx, lly, urx, ury, rotation, width, height)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: the "mirrors Page#rotate(0, flatten: true)" line is accurate, and I'd add the caveat that makes it true. Hexapdf builds its matrix from box edges; this takes extents, and the two coincide only because the caller hands over coordinates already normalized to a 0-origin box.

Half a sentence in the comment saves the next person the trip through correct_coordinates that I just took.

@bernardodsanderson
bernardodsanderson merged commit c0af6f3 into main Aug 4, 2026
5 checks passed
@bernardodsanderson
bernardodsanderson deleted the CP-14715 branch August 4, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants