Skip to content

Commit

Permalink
Merge checkbox and radio management in PDF
Browse files Browse the repository at this point in the history
This way works with many PDF readers and validators.
  • Loading branch information
liZe committed May 13, 2024
1 parent a46b704 commit ab6f85c
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 73 deletions.
4 changes: 2 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,9 @@ def test_partial_pdf_custom_metadata():
('<input value="">', ['/Tx', '/V ()']),
('<input type="checkbox">', ['/Btn']),
('<input type="radio">',
['/Btn', '/V /Off', '/AS /Off', f'/Ff {1 << (16 - 1)}']),
['/Btn', '/V /Off', '/AS /Off', '/Ff 49152']),
('<input checked type="radio" name="foo" value="value">',
['/Btn', '/T (foo)', '/V /dmFsdWU=', '/AS /dmFsdWU=']),
['/Btn', '/T (1)', '/V /dmFsdWU=', '/AS /dmFsdWU=']),
('<form><input type="radio" name="foo" value="v0"></form>'
'<form><input checked type="radio" name="foo" value="v1"></form>',
['/Btn', '/AS /djE=', '/V /djE=', '/AS /Off', '/V /Off']),
Expand Down
120 changes: 49 additions & 71 deletions weasyprint/pdf/anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,30 +93,6 @@ def add_outlines(pdf, bookmarks, parent=None):
return outlines, count


def _make_checked_stream(resources, rectangle, compress, style, font_size,
character):
width = rectangle[2] - rectangle[0]
height = rectangle[1] - rectangle[3]
on_stream = pydyf.Stream(extra={
'Resources': resources.reference,
'Type': '/XObject',
'Subtype': '/Form',
'BBox': pydyf.Array((0, 0, width, height)),
}, compress=compress)
on_stream.push_state()
on_stream.begin_text()
on_stream.set_color_rgb(*style['color'][:3])
on_stream.set_font_size('ZaDb', font_size)
# Center (let’s assume that Dingbat’s check has a 0.8em size)
x = (width - font_size * 0.8) / 2
y = (height - font_size * 0.8) / 2
on_stream.move_text_to(x, y)
on_stream.show_text_string(character)
on_stream.end_text()
on_stream.pop_state()
return on_stream


def add_forms(forms, matrix, pdf, page, resources, stream, font_map,
compress):
"""Include form inputs in PDF."""
Expand Down Expand Up @@ -154,68 +130,70 @@ def add_forms(forms, matrix, pdf, page, resources, stream, font_map,
font_size = style['font_size'] * 0.75
field_stream = pydyf.Stream(compress=compress)
field_stream.set_color_rgb(*style['color'][:3])
if input_type == 'checkbox':
# Checkboxes
checked_stream = _make_checked_stream(
resources, rectangle, compress, style, font_size,
'4') # Checkbox character in Dingbats
if input_type in ('radio', 'checkbox'):
if input_type == 'radio':
if input_name not in radio_groups[form]:
radio_groups[form][input_name] = group = pydyf.Dictionary({
'FT': '/Btn',
'Ff': (1 << (15 - 1)) + (1 << (16 - 1)), # NoToggle & Radio
'T': pydyf.String(f'{len(radio_groups)}'),
'V': '/Off',
'Kids': pydyf.Array(),
})
pdf.add_object(group)
pdf.catalog['AcroForm']['Fields'].append(group.reference)
group = radio_groups[form][input_name]
character = 'l' # Disc character in Dingbats
else:
character = '4' # Check character in Dingbats

# Create stream when input is checked
width = rectangle[2] - rectangle[0]
height = rectangle[1] - rectangle[3]
checked_stream = pydyf.Stream(extra={
'Resources': resources.reference,
'Type': '/XObject',
'Subtype': '/Form',
'BBox': pydyf.Array((0, 0, width, height)),
}, compress=compress)
checked_stream.push_state()
checked_stream.begin_text()
checked_stream.set_color_rgb(*style['color'][:3])
checked_stream.set_font_size('ZaDb', font_size)
# Center (let’s assume that Dingbat’s characters have a 0.75em size)
x = (width - font_size * 0.75) / 2
y = (height - font_size * 0.75) / 2
checked_stream.move_text_to(x, y)
checked_stream.show_text_string(character)
checked_stream.end_text()
checked_stream.pop_state()
pdf.add_object(checked_stream)

checked = 'checked' in element.attrib
field_stream.set_font_size('ZaDb', font_size)
key = b64encode(input_value.encode(), altchars=b"+-").decode()
field = pydyf.Dictionary({
'Type': '/Annot',
'Subtype': '/Widget',
'Rect': pydyf.Array(rectangle),
'FT': '/Btn',
'F': 1 << (3 - 1), # Print flag
'P': page.reference,
'T': pydyf.String(input_name),
'V': '/Yes' if checked else '/Off',
'AP': pydyf.Dictionary({'N': pydyf.Dictionary({
'Yes': checked_stream.reference,
})}),
'AS': '/Yes' if checked else '/Off',
'DA': pydyf.String(b' '.join(field_stream.stream)),
})
pdf.add_object(field)
elif input_type == 'radio':
if input_name not in radio_groups[form]:
new_group = pydyf.Dictionary({
'Type': '/Annot',
'Subtype': '/Widget',
'Rect': pydyf.Array(rectangle),
'FT': '/Btn',
'Ff': 1 << (16 - 1), # Radio flag
'F': 1 << (3 - 1), # Print flag
'P': page.reference,
'T': pydyf.String(input_name),
'V': '/Off',
'Kids': pydyf.Array(),
})
pdf.add_object(new_group)
page['Annots'].append(new_group.reference)
radio_groups[form][input_name] = new_group
group = radio_groups[form][input_name]
on_stream = _make_checked_stream(
resources, rectangle, compress, style, font_size,
'l') # Disc character in Dingbats
pdf.add_object(on_stream)
checked = 'checked' in element.attrib
key = b64encode(input_value.encode(), altchars=b"+-").decode()
field = pydyf.Dictionary({
'Type': '/Annot',
'Subtype': '/Widget',
'Rect': pydyf.Array(rectangle),
'Parent': group.reference,
'AS': f'/{key}' if checked else '/Off',
'AP': pydyf.Dictionary({'N': pydyf.Dictionary({
key: on_stream.reference})}),
key: checked_stream.reference})}),
'MK': pydyf.Dictionary({'CA': pydyf.String(character)}),
'DA': pydyf.String(b' '.join(field_stream.stream)),
})
if checked:
group['V'] = f'/{key}'
pdf.add_object(field)
group['Kids'].append(field.reference)
if input_type == 'radio':
field['Parent'] = group.reference
if checked:
group['V'] = f'/{key}'
group['Kids'].append(field.reference)
else:
field['T'] = pydyf.String(input_name)
field['V'] = field['AS']
elif element.tag == 'select':
# Select fields
font_description = get_font_description(style)
Expand Down

0 comments on commit ab6f85c

Please sign in to comment.