Skip to content

PPF-2112: Radio buttons are read as checkboxes when the group's default value is Off - #2115

Merged
chinapandaman merged 2 commits into
masterfrom
PPF-2112
Nov 8, 2026
Merged

chinapandaman merged 2 commits into
masterfrom
PPF-2112

Conversation

@chinapandaman

@chinapandaman chinapandaman commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Resolves #2112.

Changes

The checkbox rule in WIDGET_TYPE_PATTERNS that matches a parent /DV of /Yes or /Off ignored the radio flag, so off buttons in such a group became checkboxes. PdfWrapper(...) then either raised AttributeError or silently read the group as a checkbox.

  • A new Radio rule sits just before that checkbox rule and matches when the parent field has the radio flag (/Ff bit 16).
  • _is_value_match now accepts a callable pattern value, so a rule can test a flag bit. _is_widget and construct_widget still share the one table.
  • get_radio_value no longer treats /Off as a selected option. Without this, the new rule would make a group with /DV /Off and /V /Off report its last option as selected, and an empty fill would turn that option on. Radio groups with /V /Off and no /DV already had this bug, and it is fixed for them too.

Why not the patch proposed in the issue

It skips every Checkbox match for a radio-flagged widget with a parent. That also covers the rule for widgets with their own /FT, so a radio widget under a container field with no /FT matches nothing afterwards and disappears from widgets. On master it is a Checkbox, and it stays one here.

Reading the flag from the parent also removes the limit noted there: a button in the group with a stray /Ff of 0 is still a Radio.

Validation

  • New tests cover a /DV of /Off and /Yes with the first, second, or no button on, including .pages and a fill round trip. Others confirm that a group with /V /Off reads as unselected and stays off through an empty fill, and that a button without the radio flag stays a Checkbox. The radio cases fail without their fixes.
  • Widget types and read values for all 253 PDFs under pdf_samples/ are unchanged by the fix. None of them has a radio group with /V /Off. No expected PDFs needed regenerating.
  • coverage run -m pytest (495 passed, 100% coverage), make linting, and ruff format --check on the changed files passed, using the local ./venv with standard zlib.

#2114 reached master during this work. It only changes workflow files, so these results still apply.

🤖 Generated with Claude Code

The checkbox rule that matches a parent /DV of /Yes or /Off ignored the
radio flag, so off buttons in such a group became checkboxes. That either
crashed widget discovery or silently read the group as a checkbox.

Add a Radio rule ahead of it that checks the parent's radio flag, and let
pattern values be predicates so the rule can test a flag bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chinapandaman chinapandaman self-assigned this Nov 8, 2026
@chinapandaman chinapandaman added the bug Something isn't working label Nov 8, 2026
@chinapandaman chinapandaman moved this from Backlog to In review in PyPDFForm-support-2026 Nov 8, 2026
@chinapandaman

Copy link
Copy Markdown
Owner Author

[P2] Preserve unselected radio groups with /V /Off

The new radio classification rule exposes an existing problem in get_radio_value(): it treats a match against the /Off appearance as a selected option.

For a group with /DV /Off, /V /Off, and every button off, .data["group"] now reports the last option as selected, and fill({}) silently turns that option on. Before this change, the same input reports None and an empty fill preserves both off states.

Reproduction using this PR's helper:

from io import BytesIO
from runpy import run_path

from pikepdf import Name, Pdf
from PyPDFForm import PdfWrapper

build = run_path("tests/test_functional.py")["build_button_group"]
with Pdf.open(BytesIO(build(49152, Name.Off, None))) as pdf:
    pdf.Root.AcroForm.Fields[0].V = Name.Off
    stream = BytesIO()
    pdf.save(stream)

form = PdfWrapper(stream.getvalue())
print(form.data)  # Actual: {'group': 1}; expected: {'group': None}
with Pdf.open(BytesIO(form.fill({}).read())) as pdf:
    field = pdf.Root.AcroForm.Fields[0]
    print(field.V)  # /b; expected: /Off
    print([str(kid.AS) for kid in field.Kids])
    # Actual: ['/Off', '/b']; expected: ['/Off', '/Off']

Please exclude /Off when detecting selected radio values and add a regression test asserting the initial value and preservation through an empty fill. The current helper omits /V, so the all-off test misses this case.

Validation on e07f0f6b: 494 tests passed with 100% coverage; make linting and formatting checks passed.

get_radio_value compared the parent /V against every normal appearance,
including /Off, so a group with /V /Off reported its last option as
selected and an empty fill turned that option on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chinapandaman
chinapandaman merged commit caf69e3 into master Nov 8, 2026
21 checks passed
@chinapandaman
chinapandaman deleted the PPF-2112 branch November 8, 2026 16:52
@github-project-automation github-project-automation Bot moved this from In review to Done in PyPDFForm-support-2026 Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

PPF-2112: Radio buttons are read as checkboxes when the group's default value is Off

1 participant