Technisch artikel

Arabische en Hebreeuwse comb-velden in Delphi met HotPDF

HotPDF vult AcroForm comb-velden met Arabisch, Hebreeuws, combinatietekens en gemengd-richting-tekst via THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Het comb-raster wordt geteld in Unicode-scalars, een ligatuur of een basisletter met zijn tekens wordt als één cluster gecentreerd over de k cellen die zijn scalars bezitten, en de hele regel gaat door het Unicode Bidirectional Algorithm voordat er ook maar één cel wordt toegewezen

Elk van die drie regels vervangt een shortcut die er in een Latijns voorbeeld prima uitziet en op het eerste echte formulier uit elkaar valt. Denk aan een overheidsaanvraag met een comb-veld van tien hokjes voor een naam in het Arabisch. Vul hem met per-teken-logica en je krijgt letters in geïsoleerde vormen die van links naar rechts marcheren, een lam-alef-ligatuur in één hokje geperst terwijl het buurhokje leeg staat, en een fatha die boven de verkeerde cel zweeft. Elk van die defecten komt van de verkeerde eenheid tellen of de tekst op het verkeerde moment ordenen

Twee manieren om een Arabisch comb-veld te vullen met HotPDF in Delphi: de per-teken-shortcut plet de lam-alef-ligatuur, de shaped-vulling centreert elk cluster
Unicode-scalars tellen en de regel herordenen voordat cellen worden toegewezen, houdt de ligatuur gecentreerd over zijn twee hokjes en laat geen enkele cel leeg

Wat vraagt de Comb-vlag een PDF-writer eigenlijk te doen?

De Comb-vlag vraagt de writer het veld op te splitten in precies MaxLen gelijkmatig verdeelde posities en in elk één teken te zetten. ISO 32000-1 §12.7.4.3 definieert Comb als bitpositie 25 in Table 228, de veldvlaggen die specifiek zijn voor tekstvelden, en maakt hem alleen betekenisvol wanneer de MaxLen-entry uit Table 229 aanwezig is en de vlaggen Multiline, Password en FileSelect uit staan. Het bit zelf is $1000000 in de 32-bit /Ff-waarde; als je weleens comb-velden hebt zien ontregelen na een roundtrip door een floating-point-variabele, dan legt het artikel over 32-bit AcroForm-veldvlaggen uit waarom dat bit de eerste is die wegvalt

De specificatie zegt "teken" en houdt daar op. Ze is geschreven met Latijnse boxen met vaste spatie in gedachten en zegt niet wat een teken is wanneer een Arabisch letterpaar één glyph wordt, of wanneer een klinkerteken geen eigen breedte heeft. HotPDF moet een eenheid kiezen, en daarbij handhaaft hij de structurele regels streng: een comb-veld dat ook multiline is, of dat geen positieve MaxLen draagt, wordt met een diagnose geweigerd, en wachtwoordvelden kunnen helemaal geen comb-velden zijn

Waarom Unicode-scalars tellen in plaats van UTF-16-units, glyphs of graphemes?

HotPDF telt comb-cellen in Unicode-scalars omdat dat dezelfde eenheid is waarmee hij MaxLen afdwingt, en raster en lengtecontrole moeten het eens zijn, anders past een waarde die door de validatie komt niet in zijn hokjes. De drie verleidelijke alternatieven breken elk op een eigen manier die overeenkomst

  • UTF-16-codeunits zijn wat de Length van Delphi teruggeeft, dus een supplementair teken zoals U+20BB7 zou twee hokjes en twee slots van het veldbudget opvreten
  • Glyphtellingen hangen van het font af: de meeste Arabische fonts maken van lam plus alef één glyph, dus het cellenaantal zou veranderen zodra je van lettertype wisselt
  • Graphemetellingen smelten een basisletter samen met zijn tekens, wat betekent dat het raster het oneens zou worden met een MaxLen-controle die elke scalar telt die de gebruiker typte

De MaxLen-validatie in THPDFHeadlessDocument loopt alleen door de string wanneer zijn UTF-16-lengte de limiet overschrijdt, en als dat zo is, telt hij een geldig surrogaanpaar één keer en weigert hij een ongepaarde surrogaan ronduit. Wil je UI waarschuwen vóór de aanroep in plaats van na afloop een exception vangen, tel dan op dezelfde manier

Matrix die UTF-16-units, glyphs en graphemes afzet tegen Unicode-scalars voor HotPDF comb-cellen in Delphi, met lam-alef, beh plus fatha, U+20BB7 en e met accent
alleen de Unicode-scalar-kolom klopt met de MaxLen-controle — een surrogaanpaar telt één keer en het cellenaantal hangt nooit van het font af
// Telt Unicode-scalars zoals het comb-raster dat doet:
// een geldig surrogaanpaar is één 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;

Een paar waarden laten zien hoe ver de eenheden uit elkaar lopen, en waarom alleen de scalar-kolom matcht met wat een comb-veld budgetteert

WaardeUTF-16-unitsScalars (cellen)Typische glyphs
Lam + alef (لا)221 ligatuur
Beh + fatha (بَ)222, teken vast aan de basis
U+20BB7 (𠮷)211
Gedecomposeerde e + U+0301221 of 2, fontafhankelijk

Hoe delen ligaturen en combinatietekens hun cellen?

Een shaped cluster die k scalars omvat, neemt k opeenvolgende cellen in en wordt als één eenheid eroverheen gecentreerd. Na het shapen wordt elk cluster teruggezet naar de exacte span brontekst waar het vandaan kwam, dus HotPDF weet welke scalars — en dus welke cellen — hij bezit. De lam-alef-ligatuur hierboven krijgt twee hokjes en zit gecentreerd over beide. Een beh met een shadda en een fatha is drie scalars, dus hij krijgt drie hokjes, met de tekens nog steeds aan de letter vast

Het centreren is waar de voor de hand liggende implementatie de mist in gaat. Elke glyph in zijn eigen hokje centreren trekt een teken van zijn basis af, want de positie van een teken betekent alleen iets relatief aan de letter die hij decoreert. HotPDF houdt elke glyph binnen een cluster op zijn shaped breedte en zijn X- en Y-offsets en centreert daarna het cluster op basis van de gecombineerde horizontale omvang van de natuurlijke breedte, de gepositioneerde glyph-oorsprongen en de werkelijke glyph-inkt. Het cluster verplaatst als blok; van binnen wordt er niets herordend

Eén gevolg is makkelijk te missen. Directionele besturingen zoals U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK en de embedding- en isolate-besturingen zijn Unicode-scalars zonder glyph. Ze tellen mee voor MaxLen en bezetten een cel die leeg blijft. Komt je brondatum uit een systeem dat deze tekens door namen strooit, strip ze dan voor het vullen, anders toont het veld een gat en weigert het waarden één teken eerder dan de gebruiker verwacht

Waarom moet bidi-herordening vóór het toewijzen van cellen plaatsvinden?

HotPDF stuurt de volledige regel eerst door het Unicode Bidirectional Algorithm (UAX #9) en geeft pas daarna elke scalar zijn cel op basis van zijn visuele positie. Cellen in logische volgorde toewijzen en het resultaat daarna omdraaien, werkt voor een pure Arabische string en gaat mis zodra de regel cijfers of een Latijns token bevat, want die runs moeten binnen de rechts-naar-links-regel van links naar rechts blijven lezen. Een waarde zoals een referentiecode met een Arabisch prefix en een nummer houdt zijn cijfers alleen in de juiste volgorde als de herordening de hele regel in één keer ziet. Het overzicht van complex-script text shaping gaat dieper in op het probleem van logische versus visuele volgorde

Comb-celtoewijzing voor een Arabische waarde met cijfers in HotPDF voor Delphi: logische volgorde draait het nummer om, terwijl UAX 9 eerst de cijfers intact houdt
draai eerst het bidirectionele algoritme over de hele regel en geef daarna elke scalar de cel op zijn visuele positie, zodat ingesloten cijfers hun volgorde houden

Uitlijning positioneert daarna het hele raster van gebruikte cellen binnen het veld. De overgeërfde /Q-entry van het veld wint wanneer die aanwezig is; Options.Alignment geldt alleen wanneer het formulier er geen heeft. Dat telt voor rechts-naar-links-formulieren: een korte Arabische waarde in een links uitgelijnd comb-veld vult de hokjes aan de linkerkant, wat spec-correct is en bijna nooit wat de formulierontwerper voor ogen had. Repareer de /Q van het formulier, of zet htaRight wanneer het veld geen eigen quadding heeft. Widget-rotatie komt uit de /MK-/R-entry, en bij 90 of 270 graden wisselt de logische tekstviewport breedte en hoogte terwijl de cellogica hetzelfde blijft

Een comb-veld met mixed fonts vullen vanuit Delphi

Comb-tekst met mixed fonts gebruikt één cellenraster, één fontgrootte en één baseline voor elk meegeleverd font. Met FallbackFontFamilies op True kiest HotPDF voor elke complete grapheme een font uit de aangeleverde families in hun ingestelde volgorde, dus een Arabisch lettertype kan de letters dragen terwijl een Latijns lettertype de ingesloten ASCII draagt. Elke font-span wordt geshaped met de hele regel als context, een teken wordt nooit van zijn basis gesplitst over fonts heen en er wordt geen ligatuur verzonnen over een fontgrens. Elke span uitzetten op zijn eigen lokale raster of opschalen naar zijn eigen bounding box zou simpeler zijn, en het zou zowel celposities als geroteerde velden breken op het moment dat fallback midden in de regel van font wisselt

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);                  // één tot zestien families
    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;        // al de default
    Options.FallbackFontFamilies := True;    // default is False
    Options.FontSize := 0;                   // auto-fit, begrensd op 12
    Options.Alignment := htaRight;           // alleen gebruikt wanneer /Q ontbreekt

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // moet verschillen van SourceFile
  finally
    Doc.Free;
  end;
end;

Kijk naar wat het voorbeeld níét zet. Bij het vullen van een document worden Width, Height, Rotation en CombCells afgeleid uit elke widgetrechthoek, zijn /MK-entry en de MaxLen van het veld, dus waarden die je in die members zet worden overschreven. Een FontSize van nul vraagt om automatische grootte, die de omvang van elk cluster toetst aan de breedte van zijn k cellen en de gedeelde metric- en inkthoogte van alle deelnemende fonts; een vaste grootte wordt gewoon bij de widget afgeknipt. CharacterSpacing en WordSpacing verplaatsen comb-posities nooit, want het raster alleen bepaalt waar een cluster landt. HotPDF laadt zijn HarfBuzz- en FriBidi-runtimes pas wanneer een waarde echt shaping vereist, en waarden die geen shaping nodig houden de bestaande directe comb-layout ongewijzigd. Voor de algemene werkflow voor het vullen van geladen PDF's, inclusief permissies en opslaan, zie formulierveldwaarden zetten in een geladen PDF

Wat garandeert een shaped comb-appearance niet?

Een shaped comb-appearance garandeert correcte celtoewijzing, intacte clusters en een bewaarde logische waarde; hij garandeert geen naadloze cursieve verbindingen of perfecte tekstextractie in elke tool. Beide grenzen volgen uit wat een comb-veld is, en je moet ze kennen voordat je een klant iets belooft

Cursieve verbindingen over cellen zijn de eerste. Arabische letters worden nog steeds geshaped met de volledige regel als context, dus elk krijgt zijn juiste begin-, midden- of eindvorm, maar de vaste celspatiëring zet ruimte tussen aparte clusters en de verbindende streken komen elkaar niet tegen zoals in doorlopende tekst. Dat zit inherent aan het in hokjes tekenen van een verbindend schrift, en geen enkel centreren verbergt dat

Tekstextractie is de tweede. De /V van het veld bewaart de kale logische waarde, en de ouder-Form XObject van de appearance draagt één /ActualText-span (ISO 32000-1 §14.9.4) met de volledige input; de kinder-Forms dragen geen. Extractoren die ActualText respecteren, halen de originele string terug. Extractoren die hem negeren, bouwen tekst op uit glyphposities en kunnen een andere volgorde of dubbele clustertekst teruggeven, want een cluster met meerdere glyphs heeft geen enkele eenduidige Unicode-waarde per glyph. Heeft een stroomafwaarts systeem de waarde nodig, lees dan het veld, niet de paginatekst

De overige grenzen zijn mechanisch. Comb-tekst moet één regel zonder tabs zijn, en een waarde met meer scalars dan cellen faalt in plaats van afgekapt te worden. Mislukkingen zijn transactioneel: een ontbrekende glyph, een ongeldig font, een budgetoverschrijding of een annulering laat de vorige waarde en appearances onaangetast, dus een geweigerde vulling laat nooit een half getekend veld achter

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;
// Bij elke fout houdt het veld zijn vorige waarde en appearance

Waar shaped comb-velden in een formulierpijplijn passen

Shaped comb-ondersteuning telt het meest voor formulieren die al bestaan: belastingaangiften, visumaanvragen, onboardingpakketten van banken en ID-registraties, ontworpen met hokjesvelden en nu gevuld in het Arabisch, Hebreeuws, Perzisch of Urdu. Tel in scalars, bewaar de originele waarde in /V, laat één raster elk font besturen en test met een waarde die rechts-naar-links-letters, cijfers en een combinatieteken mengt, want die ene string beroept elke regel uit dit artikel. De headless document-API, comb-shaping en font-fallback die hier zijn beschreven, maken deel uit van de HotPDF Delphi PDF-component