Skip to content

Commit

Permalink
Added support for Select fields as combo boxes
Browse files Browse the repository at this point in the history
I've decoded two PDFs, one with the combo box and another blank and extracted the binary info necessary to render it and applied to the `anchors.add_inputs` function.
  • Loading branch information
VagnerNico committed Nov 22, 2023
1 parent 4f322a2 commit 21d25bd
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 8 deletions.
4 changes: 2 additions & 2 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ The ``resize``, ``cursor``, ``caret-*`` and ``nav-*`` properties are **not**
supported.

The ``appearance`` property is supported. When set to ``auto``, it displays
form fields as PDF form fields (supported for text inputs, check boxes and
text areas only).
form fields as PDF form fields (supported for text inputs, check boxes, text
areas, and select only).

The ``accent-color`` property is **not** supported.
20 changes: 14 additions & 6 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,15 +482,23 @@ def test_partial_pdf_custom_metadata():
assert b'value' in stdout


@pytest.mark.parametrize('html, field', (
(b'<input>', b'/Tx'),
(b'<input type="checkbox">', b'/Btn'),
(b'<textarea></textarea>', b'/Tx'),
@pytest.mark.parametrize('html, fields', (
(b'<input>', [b'/Tx']),
(b'<input type="checkbox">', [b'/Btn']),
(b'<textarea></textarea>', [b'/Tx']),
(b'<select><option value="a">A</option></select>', [b'/Ch', b'/Opt']),
# The selected values will be (b) and (c) in the PDF.
(b'<select multiple>'
b'<option value="a">A</option>'
b'<option value="b" selected>B</option>'
b'<option value="c" selected>C</option>'
b'</select>'
, [b'/Ch', b'/Opt', b'[(b) (c)]']),
))
def test_pdf_inputs(html, field):
def test_pdf_inputs(html, fields):
stdout = _run('--pdf-forms --uncompressed-pdf - -', html)
assert b'AcroForm' in stdout
assert field in stdout
assert all(field in stdout for field in fields)
stdout = _run('--uncompressed-pdf - -', html)
assert b'AcroForm' not in stdout

Expand Down
23 changes: 23 additions & 0 deletions weasyprint/layout/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,29 @@ def block_container_layout(context, box, bottom_space, skip_stack,
if next_page['page'] is None:
next_page['page'] = new_box.page_values()[1]

if (
box.element and
box.element.tag == 'select'
):
if (
box.element.attrib.get('multiple') is not None and
not box.element.attrib.get('height') and
not box.element.attrib.get('max-height')
):
# Overriding the height when the select has the multiple attribute
# and no height attribute
options_number = len([child for child in box.element])
new_box.height = min(box.height * 4, box.height * options_number)

if (
(padding_right:=box.style.get("padding_right")) and
(padding_left:=box.style.get("padding_left")) and
padding_right > padding_left
):
# Overriding padding right because it's greater due to the select
# arrow
new_box.style["padding_right"] = padding_left

return (
new_box, resume_at, next_page, adjoining_margins, collapsing_through,
max_lines)
Expand Down
73 changes: 73 additions & 0 deletions weasyprint/pdf/anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,79 @@ def add_inputs(inputs, matrix, pdf, page, resources, stream, font_map,
'AS': '/Yes' if checked else '/Off',
'DA': pydyf.String(b' '.join(field_stream.stream)),
})
elif element.tag == "select":
# Text, password, textarea, files, and unknown
font_description = get_font_description(style)
font = pango.pango_font_map_load_font(
font_map, context, font_description)
font = stream.add_font(font)
font.used_in_forms = True

field_stream.set_font_size(font.hash, font_size)
multiple = element.attrib.get("multiple") is not None
options = []
selected_values = []
for option in element:
options.append(
pydyf.Array(
[f'({option.attrib.get("value")})', f'({option.text})']
)
)
if option.attrib.get("selected") is not None:
selected_values.append(
pydyf.String(option.attrib.get("value"))
)

if multiple:
field = pydyf.Dictionary({
'DA': pydyf.String(b' '.join(field_stream.stream)),
'F': 2 ** (3 - 1), # Print flag
'FT': '/Ch',
# To be a multiselect list we need to set the 21st bit to 1
# outputing 2097152:
# https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf # noqa: E501
# This specification can be found on the link above on page
# 445.
'Ff': (1 << 21),
'Opt': pydyf.Array(options),
'P': page.reference,
'Rect': pydyf.Array(rectangle),
'Subtype': '/Widget',
'T': pydyf.String(input_name),
'Type': '/Annot',
# The select value is a list of selected values that come
# from the options with the selected property. If there are
# no selected values, then the value is an empty string.
'V': pydyf.Array(selected_values)
if len(selected_values)
else pydyf.String(''),
})
else:
field = pydyf.Dictionary({
'DA': pydyf.String(b' '.join(field_stream.stream)),
'F': 2 ** (3 - 1), # Print flag
'FT': '/Ch',
# To be a combo box we need to set the 17th bit to 1
# outputing 131072:
# https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf # noqa: E501
# This specification can be found on the link above on page
# 444.
'Ff': (1 << 17),
'Opt': pydyf.Array(options),
'P': page.reference,
'Rect': pydyf.Array(rectangle),
'Subtype': '/Widget',
'T': pydyf.String(input_name),
'Type': '/Annot',
# Get the last value in the list when there are multiple
# selected values in a single select box (this is the same
# approach used by browsers).
# If there are no selected values, then the value is an
# empty string.
'V': selected_values[-1]
if len(selected_values)
else pydyf.String(''),
})
else:
# Text, password, textarea, files, and unknown
font_description = get_font_description(style)
Expand Down

0 comments on commit 21d25bd

Please sign in to comment.