Technischer Artikel

Arabische und hebräische Comb-Felder in Delphi mit HotPDF

HotPDF füllt AcroForm-Comb-Felder mit Arabisch, Hebräisch, kombinierenden Zeichen und Mischrichtungs-Text über THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Das Comb-Gitter wird in Unicode-Skalaren gezählt, eine Ligatur oder ein Basisbuchstabe mit seinen Zeichen wird als ein Cluster über die k Zellen zentriert, die seine Skalare besitzen, und die ganze Zeile läuft durch den Unicode-Bidirectional-Algorithmus, bevor irgendeine Zelle vergeben wird

Jede dieser drei Regeln ersetzt einen Kurzschluss, der in einer lateinischen Demo gut aussieht und am ersten echten Formular zerfällt. Stell dir einen Behördenantrag mit einem zehnkästigen Comb-Feld für einen arabisch geschriebenen Namen vor. Füll ihn mit Zeichen-für-Zeichen-Logik, und du bekommst Buchstaben in isolierten Formen, die von links nach rechts marschieren, eine Lam-Alef-Ligatur in ein Kästchen gequetscht, während der Nachbar leer bleibt, und ein Fatha, das über der falschen Zelle schwebt. Jeder dieser Mängel stammt daraus, die falsche Einheit zu zählen oder den Text im falschen Moment zu ordnen

Zwei Wege, ein arabisches Comb-Feld mit HotPDF in Delphi zu füllen: der Zeichen-für-Zeichen-Kurzschluss quetscht die Lam-Alef-Ligatur, die geformte Füllung zentriert jeden Cluster
Unicode-Skalare zu zählen und die Zeile neu zu ordnen, bevor Zellen vergeben werden, hält die Ligatur über ihren zwei Kästchen zentriert und lässt keine Zelle leer

Was verlangt das Comb-Flag eigentlich von einem PDF-Writer?

Das Comb-Flag verlangt vom Writer, das Feld in exakt MaxLen gleichmäßig verteilte Positionen zu teilen und in jede ein Zeichen zu setzen. ISO 32000-1 §12.7.4.3 definiert Comb als Bitposition 25 in Table 228, den feldspezifischen Flags für Textfelder, und macht es nur bedeutungsvoll, wenn der MaxLen-Eintrag aus Table 229 vorhanden ist und die Flags Multiline, Password und FileSelect gelöscht sind. Das Bit selbst ist $1000000 im 32-Bit-/Ff-Wert; falls du je gesehen hast, wie Comb-Felder nach einem Umweg über eine Fließkomma-Variable spinnen, erklärt der Artikel zu 32-Bit-AcroForm-Feldflags, warum dieses Bit das erste ist, das verloren geht

Die Spezifikation sagt "Zeichen" und hört dort auf. Geschrieben mit Latin-Kästen fester Breite im Hinterkopf, sagt sie nicht, was ein Zeichen ist, wenn ein arabisches Buchstabenpaar zu einem Glyph wird oder wenn ein Vokalzeichen keine eigene Breite hat. HotPDF muss eine Einheit wählen, und es setzt dabei die Strukturregeln streng durch: Ein Comb-Feld, das auch Multiline ist oder kein positives MaxLen trägt, wird mit einer Diagnose abgewiesen, und Passwortfelder können gar keine Comb-Felder sein

Warum Unicode-Skalare zählen statt UTF-16-Einheiten, Glyphen oder Grapheme?

HotPDF zählt Comb-Zellen in Unicode-Skalaren, weil das dieselbe Einheit ist, mit der es MaxLen durchsetzt, und Gitter und Längenprüfung müssen übereinstimmen, sonst passt ein Wert, der die Validierung besteht, nicht in seine Kästchen. Die drei verführerischen Alternativen brechen diese Übereinstimmung je auf eigene Weise

  • UTF-16-Codeeinheiten sind das, was Delphis Length liefert, also würde ein Supplementary-Zeichen wie U+20BB7 zwei Kästchen und zwei Slots des Feldbudgets fressen
  • Glyphenzählungen hängen vom Font ab: Die meisten arabischen Fonts machen aus Lam plus Alef ein Glyph, also würde die Zellenzahl schon beim Wechsel der Schriftart springen
  • Graphemzählungen verschmelzen einen Basisbuchstaben mit seinen Zeichen, das heißt, das Gitter würde mit einer MaxLen-Prüfung kollidieren, die jeden vom Anwender getippten Skalar zählt

Die MaxLen-Validierung in THPDFHeadlessDocument geht den String nur durch, wenn seine UTF-16-Länge das Limit überschreitet, und dann zählt sie ein gültiges Surrogate-Pair einmal und weist einen unpaarigen Surrogate rundweg ab. Wenn deine UI vor dem Aufruf warnen will, statt danach eine Exception zu fangen, zähle genauso

Matrix, die UTF-16-Einheiten, Glyphen und Grapheme gegen Unicode-Skalare für HotPDF-Comb-Zellen in Delphi vergleicht, mit Lam-Alef, Beh plus Fatha, U+20BB7 und akzentuiertem e
nur die Unicode-Skalar-Spalte deckt sich mit der MaxLen-Prüfung, ein Surrogate-Pair zählt einmal, und die Zellenzahl hängt nie vom Font ab
// Zählt Unicode-Skalare so, wie es das Comb-Gitter tut:
// ein gültiges Surrogate-Pair ist ein Skalar
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;

Ein paar Werte zeigen, wie weit die Einheiten auseinanderdriften, und warum nur die Skalar-Spalte das trifft, wofür ein Comb-Feld budgetiert

WertUTF-16-EinheitenSkalare (Zellen)Typische Glyphen
Lam + alef (لا)221 Ligatur
Beh + fatha (بَ)222, Zeichen am Basisbuchstaben
U+20BB7 (𠮷)211
Zerlegtes e + U+0301221 oder 2, fontabhängig

Wie teilen sich Ligaturen und kombinierende Zeichen ihre Zellen?

Ein geformter Cluster, der k Skalare umfasst, belegt k zusammenhängende Zellen und wird als eine Einheit über sie zentriert. Nach dem Shaping wird jeder Cluster auf den exakten Quelltext-Abschnitt zurückgemappt, aus dem er kam, also weiß HotPDF, welche Skalare und damit welche Zellen ihm gehören. Die Lam-Alef-Ligatur oben bekommt zwei Kästchen und sitzt über beide zentriert. Ein Beh, der ein Schadda und ein Fatha trägt, sind drei Skalare, also bekommt er drei Kästchen, die Zeichen bleiben am Buchstaben

Beim Zentrieren geht die naheliegende Implementierung schief. Jedes Glyph in seinem eigenen Kästchen zu zentrieren reißt ein Zeichen von seinem Basisbuchstaben weg, denn die Position des Zeichens bedeutet nur etwas relativ zu dem Buchstaben, den es schmückt. HotPDF behält jedes Glyph in einem Cluster bei seinem geformten Advance und seinen X- und Y-Offsets und zentriert dann den Cluster über die kombinierte horizontale Ausdehnung aus natürlichem Advance, positionierten Glyph-Origins und der tatsächlichen Glyphen-Tinte. Der Cluster wandert als Block; nichts in ihm wird umsortiert

Eine Folge wird leicht übersehen. Richtungssteuerzeichen wie U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK und die Embedding- und Isolate-Controls sind Unicode-Skalare ohne Glyph. Sie gehen aufs MaxLen und belegen eine Zelle, die leer bleibt. Stammen deine Quelldaten von einem System, das solche Zeichen in Namen verstreut, stripp sie vor dem Füllen, sonst zeigt das Feld eine Lücke und weist Werte ein Zeichen früher ab, als der Anwender erwartet

Warum muss die Bidi-Neuordnung vor der Zellvergabe passieren?

HotPDF schickt die komplette Zeile zuerst durch den Unicode-Bidirectional-Algorithmus (UAX #9) und gibt erst danach jedem Skalar seine Zelle aus der visuellen Position. Zellen in logischer Ordnung zu vergeben und das Ergebnis nachträglich zu spiegeln, funktioniert für einen reinen arabischen String und scheitert, sobald die Zeile Ziffern oder ein lateinisches Token enthält, denn jene Läufe müssen innerhalb der rechtsnachlinks-Zeile von links nach rechts lesbar bleiben. Ein Wert wie ein Referenzcode mit arabischem Präfix und einer Nummer behält seine Ziffern nur in der richtigen Ordnung, wenn die Neuordnung die ganze Zeile auf einmal sieht. Die Übersicht zum Text-Shaping komplexer Schriften behandelt das Problem logischer versus visueller Ordnung tiefer

Comb-Zellvergabe für einen arabischen Wert mit Ziffern in HotPDF für Delphi: logische Ordnung dreht die Nummer um, während UAX 9 zuerst die Ziffern intakt hält
fahre zuerst den Bidirectional-Algorithmus über die ganze Zeile, gib dann jedem Skalar die Zelle an seiner visuellen Position, damit eingebettete Ziffern ihre Ordnung behalten

Die Ausrichtung positioniert danach das ganze Gitter der belegten Zellen im Feld. Der geerbte /Q-Eintrag des Felds gewinnt, wenn vorhanden; Options.Alignment greift nur, wenn das Formular keinen hat. Das zählt für rechtsnachlinks-Formulare: Ein kurzer arabischer Wert in einem linksbündigen Comb-Feld füllt die linken Kästchen, was spezifikationskorrekt ist und fast nie das, was der Formularentwurf meinte. Repariere das /Q des Formulars oder setze htaRight, wenn das Feld kein eigenes Quadding hat. Die Widget-Rotation kommt aus dem /MK-/R-Eintrag, und bei 90 oder 270 Grad tauscht der logische Text-Viewport Breite und Höhe, während die Zelllogik gleich bleibt

Ein Mischfont-Comb-Feld aus Delphi füllen

Mischfont-Comb-Text nutzt ein Zellgitter, eine Fontgröße und eine Grundlinie für jeden gelieferten Font. Mit FallbackFontFamilies auf True wählt HotPDF für jedes vollständige Graphem einen Font aus den gelieferten Familien in deren konfigurierter Reihenfolge, sodass eine arabische Schrift die Buchstaben tragen kann, während eine lateinische den eingebetteten ASCII trägt. Jeder Font-Span wird mit der ganzen Zeile als Kontext geformt, ein Zeichen wird nie über Fonts hinweg von seinem Basisbuchstaben getrennt, und über eine Font-Grenze wird keine Ligatur erfunden. Jeden Span mit eigenem lokalem Gitter zu layouten oder auf seine eigene Bounding-Box zu skalieren wäre einfacher, und es würde sowohl Zellpositionen als auch rotierte Felder brechen, sobald der Fallback mitten in der Zeile den Font wechselte

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   // Bit 25: Comb
      raise Exception.Create('Not a comb field');

    SetLength(Families, 2);                  // ein bis sechzehn Familien
    Families[0].FamilyName := 'Noto Naskh Arabic';
    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;        // bereits der Default
    Options.FallbackFontFamilies := True;    // Default ist False
    Options.FontSize := 0;                   // Auto-Fit, gedeckelt bei 12
    Options.Alignment := htaRight;           // nur verwendet, wenn /Q fehlt

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // muss sich von SourceFile unterscheiden
  finally
    Doc.Free;
  end;
end;

Beachte, was das Beispiel nicht setzt. Beim Dokument-Füllen werden Width, Height, Rotation und CombCells aus jedem Widget-Rechteck, seinem /MK-Eintrag und dem MaxLen des Felds abgeleitet, also werden Werte, die du in diese Member schreibst, überschrieben. Ein FontSize von null fragt nach automatischer Größe, die die Ausdehnung jedes Clusters gegen die Breite seiner k Zellen und die gemeinsame Metrik- und Tintenhöhe aller beteiligten Fonts prüft; eine feste Größe wird schlicht aufs Widget beschnitten. CharacterSpacing und WordSpacing verschieben nie Comb-Positionen, das Gitter allein entscheidet, wo ein Cluster landet. HotPDF lädt seine HarfBuzz- und FriBidi-Laufzeiten nur, wenn ein Wert tatsächlich Shaping verlangt, und Werte ohne Shaping-Bedarf behalten das bestehende direkte Comb-Layout unverändert. Für den allgemeinen Füll-Workflow bei geladenen PDFs, inklusive Berechtigungen und Sichern, siehe Formularfeldwerte in einem geladenen PDF setzen

Was garantiert eine geformte Comb-Optik nicht?

Eine geformte Comb-Optik garantiert korrekte Zellvergabe, intakte Cluster und einen erhaltenen logischen Wert; sie garantiert nicht nahtlose kursiven Verbindungen oder perfekte Textextraktion in jedem Werkzeug. Beide Grenzen folgen daraus, was ein Comb-Feld ist, und du solltest sie kennen, bevor du einem Kunden irgendwas versprichst

Erstens die kursiven Verbindungen über Zellen. Arabische Buchstaben werden weiterhin mit Kontext der ganzen Zeile geformt, jeder bekommt also seine korrekte Anfangs-, Mittel- oder Endform, aber der feste Zellabstand setzt Raum zwischen getrennte Cluster, und die Verbindungslinien treffen sich nicht so wie in fortlaufendem Text. Das steckt im Wesen, eine verbindende Schrift in Kästchen zu zeichnen, und noch so viel Zentrieren versteckt es nicht

Zweitens die Textextraktion. Das /V des Felds bewahrt den schlichten logischen Wert, und das Eltern-Form-XObject der Optik trägt einen /ActualText-Span (ISO 32000-1 §14.9.4) mit der vollständigen Eingabe; die Kinder-Forms tragen keinen. Extraktoren, die ActualText beachten, holen den Original-String zurück. Extraktoren, die ihn ignorieren, bauen den Text aus Glyph-Positionen nach und liefern womöglich eine andere Ordnung oder duplizierten Cluster-Text, denn ein Multi-Glyph-Cluster hat keinen einzelnen naheliegenden Unicode-Wert je Glyph. Braucht ein Downstream-System den Wert, lies das Feld, nicht den Seitentext

Die übrigen Grenzen sind mechanisch. Comb-Text muss eine einzelne Zeile ohne Tabs sein, und ein Wert mit mehr Skalaren als Zellen scheitert, statt abgeschnitten zu werden. Fehler sind transaktional: Ein fehlendes Glyph, ein ungültiger Font, eine Budgetüberschreitung oder ein Abbruch lässt den vorherigen Wert und die Optiken unangetastet, ein abgewiesenes Füllen hinterlässt also nie ein halb gezeichnetes Feld

try
  Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
  on E: EHPDFHeadlessDocumentError do
    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;
// Bei jedem der Fehler behält das Feld seinen vorherigen Wert und seine Optik

Wo geformte Comb-Felder in eine Formular-Pipeline passen

Geformte Comb-Unterstützung zählt am meisten bei Formularen, die es schon gibt: Steuererklärungen, Visumanträge, Bank-Onboarding-Pakete und Ausweisregistrierungen, entworfen mit Kästchenfeldern und jetzt gefüllt auf Arabisch, Hebräisch, Persisch oder Urdu. Zähle in Skalaren, bewahre den Originalwert in /V, lass ein Gitter über alle Fonts herrschen, und teste mit einem Wert, der rechtsnachlinks-Buchstaben, Ziffern und ein kombinierendes Zeichen mischt, denn dieser eine String trainiert jede Regel dieses Artikels. Die hier beschriebene Headless-Document-API, das Comb-Shaping und der Font-Fallback sind Teil der HotPDF Delphi PDF component