# How Docuseal Handles PDF Form Field Types and Options

> Learn how Docuseal uses HexaPDF to parse PDF form field types like text, checkbox, radio, and select, normalizing geometry and options for seamless form processing.

- Repository: [DocuSeal/docuseal](https://github.com/docusealco/docuseal)
- Tags: deep-dive
- Published: 2026-05-05

---

**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`](https://github.com/docusealco/docuseal/blob/main/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:

```ruby
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`](https://github.com/docusealco/docuseal/blob/main/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_box`** → `type: 'checkbox'` with `default_value` set if `field.field_value` is present
- **`:Ch`** + **`:combo_box`** or **`:editable_combo_box`** → `type: 'select'`
- **`:Ch`** + **`:multi_select`** → `type: 'multiple'`
- **`:Tx`** + **`:comb_text_field`** → `type: 'cells'`
- **`:Tx`** with date script (`AFDate_`) → `type: 'date'` (format extracted via `DATE_FORMAT_REGEXP`)
- **`:Tx`** (fallback) → `type: 'text'`
- **`:Sig`** → `type: '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:

```ruby
{
  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`](https://github.com/docusealco/docuseal/blob/main/lib/templates/process_document.rb) (lines 73–78) specifically targets combo boxes:

```ruby
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`](https://github.com/docusealco/docuseal/blob/main/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.