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_button with options present → type: 'radio'
  • :Btn + :check_boxtype: 'checkbox' with default_value set if field.field_value is present
  • :Ch + :combo_box or :editable_combo_boxtype: 'select'
  • :Ch + :multi_selecttype: 'multiple'
  • :Tx + :comb_text_fieldtype: 'cells'
  • :Tx with date script (AFDate_) → type: 'date' (format extracted via DATE_FORMAT_REGEXP)
  • :Tx (fallback) → type: 'text'
  • :Sigtype: 'signature' or type: 'initials' (determined by field name containing "initials")

Additional metadata is captured during this phase:

  • required: Set to true when field.flags.include?(:required)
  • default_value: Stored only when non-empty and not matching SELECT_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:

  1. Select placeholders: Values matching SELECT_PLACEHOLDER_REGEXP (e.g., "Select...") are stripped to prevent false defaults
  2. 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:

  1. Upload Phase: Templates::ProcessDocument.call detects the PDF MIME type (PDF_CONTENT_TYPE) and instantiates HexaPDF::Document when field extraction is requested
  2. Extraction Phase: Templates::FindAcroFields.call(pdf, attachment, data) returns an array of canonical field descriptors containing type, options, geometry, and default values
  3. 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.rb contains 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_REGEXP and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →