Article technique

Champs comb arabe et hébreu en Delphi avec HotPDF

HotPDF remplit les champs comb AcroForm avec de l'arabe, de l'hébreu, des signes combinants et du texte à directions mixtes via THPDFHeadlessDocument.SetTextFieldValueWithFamilies. La grille comb se compte en scalaires Unicode, une ligature ou une lettre de base avec ses signes est centrée comme un seul cluster sur les k cellules que ses scalaires possèdent, et la ligne entière passe par l'algorithme bidirectionnel Unicode avant que la moindre cellule ne soit attribuée

Chacune de ces trois règles remplace un raccourci qui a l'air très bien dans une démo latine et se désintègre sur le premier vrai formulaire. Imaginez un dossier administratif avec un champ comb de dix cases pour un nom écrit en arabe. Remplissez-le avec une logique par caractère et vous obtenez des lettres en formes isolées défilant de gauche à droite, une ligature lam-alef entassée dans une case pendant que sa voisine reste vide, et un fatha flottant au-dessus de la mauvaise cellule. Chacun de ces défauts vient du comptage de la mauvaise unité ou de l'ordonnancement du texte au mauvais moment

Deux façons de remplir un champ comb arabe avec HotPDF en Delphi : le raccourci par caractère écrase la ligature lam-alef, le remplissage shape centre chaque cluster
compter les scalaires Unicode et réordonner la ligne avant l'attribution des cellules garde la ligature centrée sur ses deux cases et ne laisse aucune cellule vide

Que demande réellement le drapeau Comb à un writer PDF ?

Le drapeau Comb demande au writer de découper le champ en exactement MaxLen positions également espacées et de mettre un caractère dans chacune. L'ISO 32000-1 §12.7.4.3 définit Comb comme bit 25 de la table 228, les drapeaux de champ propres aux champs texte, et ne le rend significatif que si l'entrée MaxLen de la table 229 est présente et que les drapeaux Multiline, Password et FileSelect sont au repos. Le bit lui-même vaut $1000000 dans la valeur /Ff 32 bits ; si vous avez déjà vu des champs comb se dérégler après un aller-retour par une variable à virgule flottante, l'article sur les drapeaux de champs AcroForm 32 bits explique pourquoi ce bit est le premier à sauter

La spécification dit « caractère » et s'arrête là. Elle a été écrite avec des cases latines à chasse fixe en tête et ne dit pas ce qu'est un caractère quand une paire de lettres arabes devient un glyphe, ou quand un signe de voyelle n'a pas de largeur propre. HotPDF doit choisir une unité, et il applique strictement les règles structurelles ce faisant : un champ comb qui est aussi multiline, ou qui ne porte pas de MaxLen positif, est rejeté avec un diagnostic, et les champs password ne peuvent pas être des champs comb du tout

Pourquoi compter en scalaires Unicode plutôt qu'en unités UTF-16, glyphes ou graphèmes ?

HotPDF compte les cellules comb en scalaires Unicode parce que c'est la même unité qu'il utilise pour faire respecter MaxLen, et la grille et le contrôle de longueur doivent concorder, sinon une valeur qui passe la validation ne tiendra pas dans ses cases. Les trois alternatives tentantes cassent chacune cette concordance d'une façon différente

  • Les unités de code UTF-16 sont ce que renvoie le Length de Delphi, donc un caractère du plan supplémentaire tel que U+20BB7 mangerait deux cases et deux emplacements du budget du champ
  • Les comptages de glyphes dépendent de la police : la plupart des polices arabes transforment lam plus alef en un seul glyphe, donc le nombre de cellules changerait dès que vous changez de police
  • Les comptages de graphèmes fusionnent une lettre de base avec ses signes, ce qui ferait désaccorder la grille d'un contrôle MaxLen qui compte chaque scalaire tapé par l'utilisateur

La validation MaxLen dans THPDFHeadlessDocument ne parcourt la chaîne que quand sa longueur UTF-16 dépasse la limite, et quand elle le fait, elle compte une paire de substitution valide une fois et rejette une substitution non appariée sans détour. Si votre UI veut avertir avant l'appel au lieu d'attraper une exception après coup, comptez de la même façon

Matrice comparant unités UTF-16, glyphes et graphèmes aux scalaires Unicode pour les cellules comb HotPDF en Delphi, avec lam-alef, beh plus fatha, U+20BB7 et e accentué
seule la colonne des scalaires Unicode concorde avec le contrôle MaxLen — une paire de substitution compte une fois et le nombre de cellules ne dépend jamais de la police
// Compte les scalaires Unicode comme le fait la grille comb :
// une paire de substitution valide vaut un scalaire
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;

Quelques valeurs montrent à quel point les unités divergent, et pourquoi seule la colonne des scalaires correspond à ce qu'un champ comb budgète

ValeurUnités UTF-16Scalaires (cellules)Glyphes typiques
Lam + alef (لا)221 ligature
Beh + fatha (بَ)222, signe attaché à la base
U+20BB7 (𠮷)211
e décomposé + U+0301221 ou 2, selon la police

Comment ligatures et signes combinants partagent-ils leurs cellules ?

Un cluster shape qui couvre k scalaires occupe k cellules contiguës et est centré comme une seule unité sur elles. Après le shaping, chaque cluster est remappé vers l'étendue exacte de texte source d'où il vient, donc HotPDF sait quels scalaires, et donc quelles cellules, il possède. La ligature lam-alef ci-dessus reçoit deux cases et se tient centrée sur les deux. Un beh portant un shadda et un fatha fait trois scalaires, donc il reçoit trois cases, les signes toujours attachés à la lettre

Le centrage est là où l'implémentation évidente se trompe. Centrer chaque glyphe dans sa propre case arrache un signe à sa base, car la position du signe ne signifie quelque chose que relativement à la lettre qu'il décore. HotPDF garde chaque glyphe d'un cluster à son avance shape et ses offsets X et Y, puis centre le cluster en utilisant l'étendue horizontale combinée de l'avance naturelle, des origines des glyphes positionnés et de l'encre réelle des glyphes. Le cluster se déplace en bloc ; rien en lui n'est réordonné

Une conséquence est facile à rater. Les contrôles directionnels comme U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK, et les contrôles d'embedding et d'isolate sont des scalaires Unicode sans glyphe. Ils comptent dans le MaxLen et tiennent une cellule qui reste vide. Si vos données sources viennent d'un système qui sème ces signes dans les noms, retirez-les avant de remplir, sinon le champ montrera un trou et rejettera les valeurs un caractère plus tôt que prévu par l'utilisateur

Pourquoi le réordonnancement bidi doit-il précéder l'attribution des cellules ?

HotPDF fait passer la ligne complète par l'algorithme bidirectionnel Unicode (UAX #9) d'abord, et ne donne à chaque scalaire sa cellule depuis sa position visuelle qu'ensuite. Attribuer les cellules en ordre logique puis retourner le résultat marche pour une chaîne purement arabe et échoue dès que la ligne contient des chiffres ou un token latin, car ces segments doivent continuer à se lire de gauche à droite dans la ligne de droite à gauche. Une valeur comme un code de référence avec préfixe arabe et un nombre ne garde ses chiffres dans le bon ordre que si le réordonnancement voit la ligne entière d'un coup. La vue d'ensemble du shaping de texte complex-script couvre le problème de l'ordre logique face à visuel plus en profondeur

Attribution des cellules comb pour une valeur arabe avec chiffres dans HotPDF pour Delphi : l'ordre logique inverse le nombre, tandis que UAX 9 d'abord garde les chiffres intacts
faites tourner l'algorithme bidirectionnel sur la ligne entière d'abord, puis donnez à chaque scalaire la cellule de sa position visuelle pour que les chiffres embarqués gardent leur ordre

L'alignement positionne ensuite toute la grille des cellules utilisées dans le champ. L'entrée /Q héritée du champ gagne quand elle est présente ; Options.Alignment ne s'applique que si le formulaire n'en a pas. Cela compte pour les formulaires de droite à gauche : une courte valeur arabe dans un champ comb aligné à gauche remplit les cases de gauche, ce qui est correct au sens de la spec et presque jamais ce que le concepteur du formulaire voulait. Corrigez le /Q du formulaire, ou posez htaRight quand le champ n'a pas de quadding propre. La rotation du widget vient de l'entrée /MK /R, et à 90 ou 270 degrés le viewport logique du texte échange largeur et hauteur tandis que la logique des cellules reste la même

Remplir un champ comb multi-polices depuis Delphi

Le texte comb multi-polices utilise une grille de cellules, une taille de police et une baseline uniques pour chaque police fournie. Avec FallbackFontFamilies à True, HotPDF choisit une police pour chaque graphème complet parmi les familles fournies dans leur ordre configuré, donc une police arabe peut porter les lettres pendant qu'une police latine porte l'ASCII embarqué. Chaque segment de police est shape avec la ligne entière comme contexte, un signe n'est jamais séparé de sa base entre deux polices, et aucune ligature n'est inventée à travers une frontière de police. Composer chaque segment avec sa propre grille locale ou le mettre à l'échelle de sa propre boîte englobante serait plus simple, et cela casserait à la fois les positions de cellules et les champs pivotés dès que le fallback changerait de police en milieu de ligne

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);                  // de une à seize familles
    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;        // déjà le défaut
    Options.FallbackFontFamilies := True;    // le défaut est False
    Options.FontSize := 0;                   // ajustement auto, plafonné à 12
    Options.Alignment := htaRight;           // utilisé seulement quand /Q est absent

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // doit différer de SourceFile
  finally
    Doc.Free;
  end;
end;

Remarquez ce que l'exemple ne pose pas. Pendant le remplissage du document, Width, Height, Rotation et CombCells sont dérivés du rectangle de chaque widget, de son entrée /MK et du MaxLen du champ, donc les valeurs que vous mettez dans ces membres sont écrasées. Un FontSize zéro demande un dimensionnement automatique, qui confronte l'étendue de chaque cluster à la largeur de ses k cellules et à la hauteur métrique et d'encre partagée de toutes les polices participantes ; une taille fixe est simplement rognée au widget. CharacterSpacing et WordSpacing ne déplacent jamais les positions comb, puisque la grille seule décide où un cluster atterrit. HotPDF charge ses runtimes HarfBuzz et FriBidi seulement quand une valeur exige réellement du shaping, et les valeurs qui n'en demandent pas gardent le layout comb direct existant inchangé. Pour le workflow général de remplissage d'un PDF chargé, permissions et sauvegarde comprises, voir le positionnement des valeurs de champs de formulaire dans un PDF chargé

Que ne garantit pas une apparence comb shape ?

Une apparence comb shape garantit une attribution correcte des cellules, des clusters intacts et une valeur logique préservée ; elle ne garantit ni des liaisons cursives sans couture ni une extraction de texte parfaite dans chaque outil. Les deux limites découlent de ce qu'est un champ comb, et vous devriez les connaître avant de promettre quoi que ce soit à un client

Les liaisons cursives entre cellules sont la première. Les lettres arabes sont toujours shape avec le contexte de la ligne entière, donc chacune reçoit sa forme initiale, médiane ou finale correcte, mais le pas fixe des cellules met de l'espace entre clusters séparés et les traits de liaison ne se rejoindront pas comme dans du texte courant. C'est inhérent au dessin d'une écriture liée dans des cases, et aucun centrage ne le cache

L'extraction de texte est la seconde. Le /V du champ garde la valeur logique brute, et le Form XObject parent de l'apparence porte un span /ActualText (ISO 32000-1 §14.9.4) avec l'entrée complète ; les Forms enfants n'en portent pas. Les extracteurs qui honorent ActualText retrouvent la chaîne d'origine. Les extracteurs qui l'ignorent reconstruisent le texte depuis les positions des glyphes et peuvent renvoyer un autre ordre ou dupliquer le texte d'un cluster, car un cluster multi-glyphes n'a pas de valeur Unicode unique évidente par glyphe. Si un système aval a besoin de la valeur, lisez le champ, pas le texte de la page

Les frontières restantes sont mécaniques. Le texte comb doit tenir sur une seule ligne sans tabulations, et une valeur avec plus de scalaires que de cellules échoue plutôt que d'être tronquée. Les échecs sont transactionnels : un glyphe manquant, une police invalide, un dépassement de budget ou une annulation laissent la valeur et les apparences précédentes intactes, pour qu'un remplissage refusé ne laisse jamais derrière lui un champ à moitié dessiné

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;
// Dans les deux cas d'erreur le champ garde sa valeur et son apparence précédentes

Où les champs comb shape se situent dans un pipeline de formulaires

Le support comb shape compte surtout pour les formulaires qui existent déjà : déclarations fiscales, demandes de visa, kits d'onboarding bancaire et enregistrements d'identité conçus avec des champs à cases et désormais remplis en arabe, hébreu, persan ou ourdou. Comptez en scalaires, gardez la valeur d'origine dans le /V, laissez une seule grille gouverner chaque police, et testez avec une valeur qui mêle lettres de droite à gauche, chiffres et un signe combinant, car cette seule chaîne exerce chaque règle de cet article. L'API de document headless, le shaping comb et le fallback de polices décrits ici font partie du composant PDF HotPDF pour Delphi