Technical Article

Arabic and Hebrew Comb Fields in Delphi with HotPDF

A ten-box comb field holding the Arabic reference طلب 4821 can end up showing 1284 in its number boxes. That happens when a writer gives cells out in typing order and then mirrors the whole row because the text is right-to-left: the digits get mirrored along with the letters. The letters fare no better under a per-character loop, since each one is drawn alone in its box, in its isolated form, with a lam-alef pair crushed into a single cell. Two decisions sit behind all of it: which unit owns a cell, and when the line is reordered. A comb filler has to get both right before a single Arabic or Hebrew form looks correct

Two ways to fill an Arabic comb field with HotPDF in Delphi: the per-character shortcut crushes the lam-alef ligature, the shaped fill centers each cluster
counting Unicode scalars and reordering the line before cells are assigned keeps the ligature centered across its two boxes and leaves no cell empty

What the Comb flag asks for, and what it leaves open

The Comb flag asks a PDF writer to divide the field into exactly MaxLen equally spaced positions and put one character in each. ISO 32000-1 §12.7.4.3 defines Comb as bit position 25 in Table 228, the flags specific to text fields. It only has meaning when the MaxLen entry from Table 229 is present and the Multiline, Password and FileSelect flags are clear. In the 32-bit /Ff value the bit is $1000000. It is also the first bit to disappear when a flags value takes a detour through a floating-point variable, which the article on 32-bit AcroForm field flags walks through

The specification says "character" and stops. It was written with fixed-pitch Latin boxes in mind, and it says nothing about what a character is when an Arabic letter pair becomes one glyph or a vowel mark has no width of its own. Any implementation has to pick a unit. HotPDF picks the Unicode scalar. Its headless document API (THPDFHeadlessDocument) enforces the structural rules around it strictly: a comb field that is also multiline, or that has no positive MaxLen, raises an EHPDFHeadlessDocumentError with the code hdInvalidDocument, and a password field that also carries the Comb bit is refused the same way

Why count Unicode scalars instead of UTF-16 units, glyphs or graphemes?

THPDFHeadlessDocument.SetTextFieldValueWithFamilies counts comb cells in Unicode scalars because that is the same unit it uses to enforce MaxLen. The grid and the length check have to agree. If they don't, a value can pass validation and still not fit its boxes, and the user gets a field that looks broken for no visible reason. Each alternative fails on a concrete case

  • UTF-16 code units are what Delphi's Length returns, so a supplementary character such as U+20BB7 would take two boxes and two slots of the field budget
  • Glyph counts depend on the font: most Arabic faces turn lam plus alef into one glyph, so the number of cells would change when you switch typefaces
  • Grapheme counts merge a base letter with its marks, which leaves the grid disagreeing with a MaxLen check that counts every scalar the user typed

The MaxLen check in THPDFHeadlessDocument walks the string only when its UTF-16 length is over the limit. When it does walk, a valid surrogate pair counts once and an unpaired surrogate is rejected outright. We pinned the unit down by counting the same short values every way we could — UTF-16 units, glyphs, graphemes, scalars — and U+20BB7 settled it: Delphi's Length put it in two boxes while the scalar budget called it one. A UI that wants to warn while the user is still typing, instead of catching an exception after the call, should count exactly the same way

Matrix comparing UTF-16 units, glyphs and graphemes against Unicode scalars for HotPDF comb cells in Delphi, with lam-alef, beh plus fatha, U+20BB7 and accented e
only the Unicode scalar column agrees with the MaxLen check — a surrogate pair counts once and the cell count never depends on the font

Four short values show how far the units drift apart

ValueUTF-16 unitsScalars (cells)Typical glyphs
Lam + alef (لا)221 ligature
Beh + fatha (بَ)222, mark attached to base
U+20BB7 (𠮷)211
Decomposed e + U+0301221 or 2, font dependent

The two helpers below are not part of the library. They are code you would add to your own form layer: one counts scalars the way the comb grid does, the other removes the invisible direction marks discussed in the next section

// Your own helper, not a HotPDF API: counts Unicode scalars the
// way the comb grid and the MaxLen check do (a valid surrogate
// pair is one scalar)
function CountScalars(const S: UnicodeString): Integer;
var
  I: Integer;
begin
  Result := 0;
  I := 1;
  while I <= Length(S) do
  begin
    if (Ord(S[I]) >= $D800) and (Ord(S[I]) <= $DBFF) and
       (I < Length(S)) and
       (Ord(S[I + 1]) >= $DC00) and (Ord(S[I + 1]) <= $DFFF) then
      Inc(I, 2)
    else
      Inc(I);
    Inc(Result);
  end;
end;

// Your own helper, not a HotPDF API: drops LRM, RLM, ALM and the
// embedding / isolate controls, each of which would hold a blank cell
function StripDirectionalControls(const S: UnicodeString): UnicodeString;
var
  I: Integer;
  C: Word;
begin
  Result := '';
  for I := 1 to Length(S) do
  begin
    C := Ord(S[I]);
    case C of
      $061C, $200E, $200F, $202A..$202E, $2066..$2069: ; // skip
    else
      Result := Result + S[I];
    end;
  end;
end;

How ligatures and combining marks share their cells

A shaped cluster that covers k scalars occupies k contiguous cells and is centered as one block across them. After shaping, HotPDF maps every cluster back to the exact stretch of source text it came from, so it knows which scalars the cluster owns and therefore which cells. The lam-alef ligature gets two boxes and sits centered across both. A beh carrying a shadda and a fatha is three scalars, so it gets three boxes, and the marks stay on the letter

Centering looks like a per-glyph job. Do exactly that and a mark drifts away from its base, because a mark's position only means something relative to the letter it sits on. HotPDF keeps every glyph inside a cluster at its shaped advance and X/Y offsets, then centers the cluster using the combined horizontal extent of the natural advance, the positioned glyph origins and the real glyph ink. Nothing inside the cluster moves relative to anything else

Directional controls are the easy-to-miss case. U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK, U+200E and the embedding and isolate controls (U+202A–U+202E, U+2066–U+2069) are Unicode scalars with no glyph. Each one counts against MaxLen and holds a cell that stays blank. Strip them before filling a comb field. Text pasted from right-to-left user interfaces or exported from other systems can carry these marks invisibly, and the result is a field with an unexplained gap that rejects the value one character earlier than the user expects

Why must bidi reordering happen before cells are assigned?

HotPDF runs the complete line through the Unicode Bidirectional Algorithm (UAX #9) first and only then gives each scalar the cell at its visual position. Assigning cells in logical order and mirroring afterwards works for a pure Arabic string. It falls apart as soon as the line contains digits or a Latin token, because those runs keep reading left to right inside the right-to-left line. The reference from the opening keeps 4821 in the right order only if reordering sees the whole line at once. The complex-script text shaping overview covers logical versus visual order in more depth

Comb cell assignment for an Arabic value with digits in HotPDF for Delphi: logical order reverses the number, while UAX 9 first keeps digits intact
run the bidirectional algorithm on the whole line first, then give each scalar the cell at its visual position so embedded digits keep their order

Alignment then places the block of used cells inside the field. The field's inherited /Q entry wins when it exists; Options.Alignment applies only when no /Q is found on the field or its parents. This bites right-to-left forms in practice. A short Arabic value in a comb field whose /Q is 0 fills the leftmost boxes, which is exactly what the specification asks for and almost never what the form designer meant. Fix it in the form itself by setting /Q to 2. If you can't touch the form and it has no quadding at all, htaRight does the job from code. Widget rotation comes from the /MK /R entry, and at 90 or 270 degrees the text viewport swaps width and height while the cell logic stays the same

Filling a mixed-font comb field from Delphi

Mixed-font comb text uses one cell grid, one font size and one baseline for every supplied font. With FallbackFontFamilies set to True, HotPDF picks a font for each complete grapheme from the supplied families in order, so an Arabic face can carry the letters while a Latin face carries an embedded product code. Every font span is shaped with the whole line as context, a mark is never split from its base across fonts, and no ligature is invented across a font boundary. Giving each span its own little grid, or scaling it to its own bounding box, would be simpler to write; it would also shift cell positions and break rotated fields whenever fallback changed fonts mid-line

uses
  System.SysUtils, System.IOUtils,
  HPDFHeadlessDocument, HPDFHeadlessAppearance;

procedure FillArabicComb(const SourceFile, TargetFile, Value: string);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  Families: THPDFHeadlessRichTextFontFamilies;
  Options: THPDFHeadlessTextAppearanceOptions;
  FieldIndex: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(SourceFile);
    FieldIndex := Doc.FindFormField('applicant.name_ar');
    if FieldIndex < 0 then
      raise Exception.Create('Field not found');
    Field := Doc.GetFormField(FieldIndex);
    if (Field.Flags and $1000000) = 0 then   // Table 228 position 25: Comb
      raise Exception.Create('Not a comb field');

    SetLength(Families, 2);                  // one to sixteen families
    Families[0].FamilyName := 'Noto Naskh Arabic';  // labels must differ
    Families[0].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoNaskhArabic-Regular.ttf');
    Families[1].FamilyName := 'Noto Sans';
    Families[1].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoSans-Regular.ttf');

    Options := THPDFHeadlessTextAppearanceOptions.Default;
    Options.ShapeComplexText := True;        // already the default
    Options.FallbackFontFamilies := True;    // default is False
    Options.FontSize := 0;                   // auto-fit, capped at 12
    Options.Alignment := htaRight;           // used only when /Q is absent

    Doc.SetTextFieldValueWithFamilies(FieldIndex,
      StripDirectionalControls(Value), Families, Options);
    Doc.SaveToFile(TargetFile);              // must differ from SourceFile
  finally
    Doc.Free;
  end;
end;

Several members are missing from that example on purpose. During document filling, Width, Height, Rotation and CombCells are derived from each widget rectangle, its /MK entry and the field's MaxLen, so anything you assign to them is overwritten. Note also that the headless GetFormField hands back the document's own field record, not a fresh object, so the example never frees it; that is the opposite of the loaded-PDF THotPDF.GetFormField, whose THPDFLoadedFormField you own and must free. A zero FontSize asks for automatic sizing: every cluster's extent is checked against the width of its k cells, and the height against the shared metric and ink range of all participating fonts, with 12 points as the ceiling. A fixed size is clipped to the widget instead. CharacterSpacing and WordSpacing never move comb positions, since the grid alone decides where a cluster lands

Leave FallbackFontFamilies on for any comb field that may receive Arabic or Hebrew mixed with Latin codes. With it off, the first family has to cover every character in the value, and a single Latin letter outside an Arabic-only face turns into a missing-glyph failure at fill time. Deployment matters too. Shaping and reordering run on HarfBuzz and FriBidi, loaded the first time a value actually needs them: libharfbuzz-0.dll and libfribidi-0.dll on Windows, libharfbuzz.so.0 and libfribidi.so.0 on Linux, with HOTPDF_HARFBUZZ_LIBRARY and HOTPDF_FRIBIDI_LIBRARY overriding the paths. Set them to trusted absolute paths before the first shaped fill. A value that needs no shaping, such as plain Latin text, is laid out directly, one glyph per cell, without loading either library. For the wider loaded-PDF fill workflow, including permissions and saving, see setting form field values in a loaded PDF

What readers and text extractors see in the result

A shaped comb appearance gets three things right every time: each scalar lands in its cell, clusters stay whole, and the logical value survives. Two other properties follow from the box layout itself: how cursive joins look, and what a text extractor gets back

Cursive joins across cells are the first. Arabic letters are still shaped with full-line context, so each one takes its correct initial, medial or final form. The fixed cell pitch, though, puts space between separate clusters, and the connecting strokes stop short of each other instead of meeting as they do in running text. Centering only moves a cluster within its own k cells; the gap between neighboring clusters is fixed by the cell pitch

Text extraction is the second. The appearance carries one /ActualText span (ISO 32000-1 §14.9.4) holding the complete input, and extractors that honor ActualText recover the original string. Extractors that ignore it rebuild text from glyph positions and may return a different order or repeat cluster text, because a multi-glyph cluster has no single Unicode value per glyph. The field itself sidesteps both cases: its /V holds the plain logical value, exactly as it was passed to SetTextFieldValueWithFamilies

Comb text must be a single line without tabs. A value with more scalars than cells is refused, never truncated, and because CombCells equals MaxLen, the refusal arrives from the length check as hdLimitExceeded. A failed fill (missing glyph, invalid font, budget overrun, cancellation) leaves the previous value and appearances in place, so a rejected call cannot leave a half-drawn field in the saved file

try
  Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
  on E: EHPDFHeadlessDocumentError do
    // hdLimitExceeded also covers object and font budgets;
    // compare CountScalars(Value) with Field.MaxLength to be sure
    if E.Code = hdLimitExceeded then
      ShowFieldError('The value has more characters than the field allows')
    else
      raise;
  on E: EHPDFHeadlessAppearanceError do
    if E.Failure = haMissingGlyph then
      ShowFieldError('None of the supplied fonts covers this text')
    else
      raise;
end;
// ShowFieldError is your own UI routine; on either error the field
// keeps its previous value and appearance

Shaped comb filling pays off on forms that already exist and were designed around Latin boxes: tax returns, visa applications, bank onboarding packs and ID registrations with boxed fields, now filled in Arabic, Hebrew, Persian or Urdu. One test string exercises every rule above: an Arabic word, a four-digit number and a letter with a combining mark. Filled correctly, it shows the digits in reading order, each mark on its base letter and no blank cell between clusters. The headless document API, comb shaping and font fallback all ship with the HotPDF Delphi PDF component