How Docuseal Handles PDF Form Field Types and Options
Docuseal parses PDF AcroForm data using HexaPDF, classifies fields into canonical types like text, checkbox, radio, and select, and normalizes geometry and options via the Templates::FindAcroFields module.
When working with the docusealco/docuseal open-source repository, understanding how PDF form fields are extracted and normalized is essential for customizing template generation. The platform converts raw AcroForm annotations into structured field descriptors that drive the UI and submission logic. This article examines the Ruby implementation responsible for field detection, type mapping, and option normalization.
Field Detection and Geometry Extraction
Docuseal begins by identifying every terminal field in the PDF document. The build_fields_with_pages method within lib/templates/find_acro_fields.rb (lines 38–78) iterates over the AcroForm dictionary and computes page-relative coordinates.
Each field maps to one or more areas depending on its geometry:
- Single-rectangle fields (most text inputs) generate one area
- Multi-rectangle fields (radio button groups) generate an area per rectangle, each receiving a unique UUID
Coordinates are normalized to a 0–1 floating-point range relative to page dimensions:
areas = Array.wrap(field[:Kids] || field).filter_map do |child_field|
page = annots_index[child_field.hash]
attrs = {
page: page.index,
x: x / page_width.to_f,
y: transformed_y / page_height.to_f,
w: w / page_width.to_f,
h: h / page_height.to_f,
attachment_uuid: attachment.uuid
}
end
This normalization ensures consistent positioning across different PDF page sizes.
Mapping PDF Types to Docuseal Types
The build_field_properties method (lines 42–119 in lib/templates/find_acro_fields.rb) translates low-level PDF field types into high-level Docuseal types. The classification relies on field.field_type and field.concrete_field_type values:
:Btn+:radio_buttonwith options present →type: 'radio':Btn+:check_box→type: 'checkbox'withdefault_valueset iffield.field_valueis present:Ch+:combo_boxor:editable_combo_box→type: 'select':Ch+:multi_select→type: 'multiple':Tx+:comb_text_field→type: 'cells':Txwith date script (AFDate_) →type: 'date'(format extracted viaDATE_FORMAT_REGEXP):Tx(fallback) →type: 'text':Sig→type: 'signature'ortype: 'initials'(determined by field name containing "initials")
Additional metadata is captured during this phase:
required: Set totruewhenfield.flags.include?(:required)default_value: Stored only when non-empty and not matchingSELECT_PLACEHOLDER_REGEXP
Building and Normalizing Options
For choice-based fields, the build_options method (lines 221–239) constructs an array of option objects:
{
uuid: SecureRandom.uuid,
value: is_option_number || is_skip_single_value ? '' : option
}
The normalization logic handles two edge cases:
- Select placeholders: Values matching
SELECT_PLACEHOLDER_REGEXP(e.g., "Select...") are stripped to prevent false defaults - Single-option radio/multiple fields: When only one unique option exists, the value is cleared (
is_skip_single_value) to avoid presenting a meaningless single-choice list
Flattening Placeholder Values
When generating preview images, Docuseal removes placeholder text before flattening the PDF. The maybe_flatten_form method in lib/templates/process_document.rb (lines 73–78) specifically targets combo boxes:
next if field.field_type != :Ch ||
field[:Opt].blank? ||
%i[combo_box editable_combo_box].exclude?(field.concrete_field_type) ||
!field.field_value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
field[:V] = ''
This ensures placeholder prompts do not appear as actual values in the final rendered document.
End-to-End Processing Flow
The conversion from PDF binary to structured metadata follows three distinct phases:
- Upload Phase:
Templates::ProcessDocument.calldetects the PDF MIME type (PDF_CONTENT_TYPE) and instantiatesHexaPDF::Documentwhen field extraction is requested - Extraction Phase:
Templates::FindAcroFields.call(pdf, attachment, data)returns an array of canonical field descriptors containing type, options, geometry, and default values - Storage Phase: The descriptor array persists in
attachment.metadata['pdf']['fields'], where the template builder and submission controllers access it
Summary
- HexaPDF powers the initial AcroForm parsing, providing low-level field types and annotations
find_acro_fields.rbcontains the core classification logic, mapping PDF types to Docuseal's canonical types (text, checkbox, radio, select, multiple, date, cells, signature)- Geometry normalization converts absolute PDF coordinates to relative 0–1 values for responsive UI rendering
- Option sanitization removes placeholder values using
SELECT_PLACEHOLDER_REGEXPand handles edge cases like single-option radio groups - Preview generation strips placeholder text before flattening to ensure clean output documents
Frequently Asked Questions
How does Docuseal distinguish between radio buttons and checkboxes in PDF forms?
Docuseal checks the concrete field type via field.concrete_field_type. When the PDF type is :Btn combined with :radio_button and the field contains an options array, it becomes a radio type. If the concrete type is :check_box, it becomes a checkbox type. Both are subtypes of the PDF :Btn field type but handled differently in the UI layer.
What happens to date fields when importing a PDF?
When build_field_properties encounters a text field (:Tx) containing JavaScript date formatting (identified by the AFDate_ prefix), it extracts the format pattern using DATE_FORMAT_REGEXP and assigns type: 'date'. The extracted format string is stored in preferences[:format], allowing the frontend to render an appropriate date picker with the correct format mask.
Why are some select field values empty after import?
Docuseal intentionally clears option values when they match placeholder patterns defined by SELECT_PLACEHOLDER_REGEXP (such as "Select..." or similar prompts). Additionally, if a radio or multiple-select field contains only one unique option, the is_skip_single_value logic clears the value to prevent rendering a choice list with a single meaningless option.
How does Docuseal handle signature fields versus initial fields?
Both map from the PDF :Sig type, but Docuseal differentiates them based on the field name. If the field name contains the substring "initials", build_field_properties assigns type: 'initials'; otherwise, it assigns type: 'signature'. This allows the UI to render either a full signature pad or an initials input accordingly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →