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)