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)