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
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
Lengthliefert, 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
// 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
| Wert | UTF-16-Einheiten | Skalare (Zellen) | Typische Glyphen |
|---|---|---|---|
| Lam + alef (لا) | 2 | 2 | 1 Ligatur |
| Beh + fatha (بَ) | 2 | 2 | 2, Zeichen am Basisbuchstaben |
| U+20BB7 (𠮷) | 2 | 1 | 1 |
| Zerlegtes e + U+0301 | 2 | 2 | 1 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
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