From 21d25bdd678f4c27fe735a0a1f1243eb1048aea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vagner=20Jos=C3=A9=20Nicolodi?= Date: Fri, 3 Nov 2023 19:12:37 -0300 Subject: [PATCH] Added support for Select fields as combo boxes 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. --- docs/api_reference.rst | 4 +-- tests/test_api.py | 20 +++++++---- weasyprint/layout/block.py | 23 ++++++++++++ weasyprint/pdf/anchors.py | 73 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 1e11c758e..377c21566 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -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. diff --git a/tests/test_api.py b/tests/test_api.py index 640400462..0ca986c80 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -482,15 +482,23 @@ def test_partial_pdf_custom_metadata(): assert b'value' in stdout -@pytest.mark.parametrize('html, field', ( - (b'', b'/Tx'), - (b'', b'/Btn'), - (b'', b'/Tx'), +@pytest.mark.parametrize('html, fields', ( + (b'', [b'/Tx']), + (b'', [b'/Btn']), + (b'', [b'/Tx']), + (b'', [b'/Ch', b'/Opt']), + # The selected values will be (b) and (c) in the PDF. + (b'' + , [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 diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py index 2e1dc9958..fffb1a82d 100644 --- a/weasyprint/layout/block.py +++ b/weasyprint/layout/block.py @@ -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) diff --git a/weasyprint/pdf/anchors.py b/weasyprint/pdf/anchors.py index 269ed7b87..329f1e7a6 100644 --- a/weasyprint/pdf/anchors.py +++ b/weasyprint/pdf/anchors.py @@ -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)