Teknisk artikel

Arabiske og hebraiske comb-felter i Delphi med HotPDF

HotPDF udfylder AcroForm comb-felter med arabisk, hebraisk, combining marks og tekster med blandet retning gennem THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Comb-gitteret tælles i Unicode-scalarer, en ligatur eller et basustegn med sine mærker centreres som ét cluster hen over de k celler, dens scalarer ejer, og hele linjen køres gennem Unicode Bidirectional Algorithm, før nogen celle tildeles

Hver af de tre regler erstatter en genvej, der ser fin ud i en Latin-demo og falder fra hinanden på den første rigtige formular. Forestil dig en offentlig ansøgning med et comb-felt på ti bokse til et navn skrevet på arabisk. Udfylder du det med per-tegn-logik, får du bogstaver i isolerede former, der marcherer fra venstre mod højre, en lam-alef-ligatur presset ned i én boks, mens naboboksen står tom, og en fatha, der svæver over den forkerte celle. Hver af de defekter stammer fra at tælle den forkerte enhed eller ordne teksten på det forkerte tidspunkt

To måder at udfylde et arabisk comb-felt med HotPDF i Delphi: per-tegn-genvejen knuser lam-alef-ligaturen, den shappede udfyldning centrerer hvert cluster
at tælle Unicode-scalarer og omsortere linjen, før celler tildeles, holder ligaturen centreret hen over dets to bokse og efterlader ingen celle tom

Hvad beder Comb-flaget faktisk en PDF-writer om at gøre?

Comb-flaget beder writeren om at opdele feltet i præcis MaxLen ligeligt fordelte positioner og sætte ét tegn i hver. ISO 32000-1 §12.7.4.3 definerer Comb som bitposition 25 i Table 228, de feltflag, der gælder tekstfelter, og gør den kun meningsfuld, når MaxLen-posten fra Table 229 er til stede, og Multiline-, Password- og FileSelect-flagene er ryddet. Bitten selv er $1000000 i 32-bit /Ff-værdien; har du nogensinde set comb-felter opføre sig mærkeligt efter en tur gennem en floating-point-variabel, forklarer artiklen om 32-bit AcroForm feltflag, hvorfor netop den bit forsvinder først

Specifikationen siger "character" og stopper der. Den er skrevet med fastbredde-latinske bokse i tankerne og siger ikke, hvad et tegn er, når ét arabisk bogstavpar bliver til ét glyph, eller når et vokalmærke ikke har nogen egen bredde. HotPDF må vælge en enhed, og den håndhæver de strukturelle regler strengt undervejs: et comb-felt, der også er multiline, eller som ikke har en positiv MaxLen, afvises med en diagnostik, og passwordfelter kan slet ikke være comb-felter

Hvorfor tælle Unicode-scalarer i stedet for UTF-16-enheder, glyphs eller grafemer?

HotPDF tæller comb-celler i Unicode-scalarer, for det er samme enhed, den bruger til at håndhæve MaxLen, og gitteret og længdetjekket skal være enige, ellers passer en værdi, der består valideringen, ikke i sine bokse. De tre fristende alternativer bryder hver på sin måde den overensstemmelse

  • UTF-16-kodeenheder er det, Delphis Length returnerer, så et supplementary-tegn som U+20BB7 ville spise to bokse og to pladser af feltbudgettet
  • Glyph-antal afhænger af fonten: de fleste arabiske fonte gør lam plus alef til ét glyph, så celletallet ville ændre sig, i samme øjeblik du skifter typeface
  • Grafem-antal smelter et basustegn sammen med sine mærker, hvilket betyder, at gitteret ville stride med et MaxLen-tjek, der tæller hver scalar, brugeren tastede

MaxLen-valideringen i THPDFHeadlessDocument gennemløber kun strengen, når dens UTF-16-længde overstiger grænsen, og når den gør, tæller den et gyldigt surrogate-par én gang og afviser et uparret surrogate på stedet. Vil din UI advare før kaldet i stedet for at fange en exception efter det, så tæl på samme måde

Matrix, der sammenligner UTF-16-enheder, glyphs og grafemer med Unicode-scalarer for HotPDF comb-celler i Delphi, med lam-alef, beh plus fatha, U+20BB7 og accentueret e
kun Unicode-scalar-kolonnen er enig med MaxLen-tjekket — et surrogate-par tæller én gang, og celletallet afhænger aldrig af fonten
// Tæller Unicode-scalarer som comb-gitteret gør:
// et gyldigt surrogate-par er é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;

Et par værdier viser, hvor langt enhederne driver fra hinanden, og hvorfor kun scalar-kolonnen matcher, hvad et comb-felt budgetterer med

VærdiUTF-16-enhederScalarer (celler)Typiske glyphs
Lam + alef (لا)221 ligatur
Beh + fatha (بَ)222, mærke fastgjort til basen
U+20BB7 (𠮷)211
Decomposed e + U+0301221 eller 2, afhænger af fonten

Hvordan deler ligaturer og combining marks deres celler?

Et shappet cluster, der dækker k scalarer, optager k sammenhængende celler og centreres som én enkelt enhed hen over dem. Efter shaping mapkes hvert cluster tilbage til det præcise span af kildetekst, det kom fra, så HotPDF ved, hvilke scalarer — og dermed hvilke celler — det ejer. Lam-alef-ligaturen ovenfor får to bokse og sidder centreret hen over begge. En beh med en shadda og en fatha er tre scalarer, så den får tre bokse, med mærkerne stadig fastgjort til bogstavet

Centrering er der, hvor den åbenlyse implementation går galt. Centerer du hvert glyph i sin egen boks, trækker du et mærke væk fra sin base, for mærkets position betyder kun noget relativt til bogstavet, det pynter. HotPDF beholder hvert glyph i et cluster ved dets shappede advance og dets X- og Y-offsets og centrerer derefter clusteret ud fra den samlede vandrette udstrækning af natural advance, de positionerede glyph-origins og den faktiske glyph-ink. Clusteret flytter sig som en blok; intet indeni omrokeres

En konsekvens er let at overse. Retningskontroller som U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK og embedding- og isolate-kontrollerne er Unicode-scalarer uden glyph. De tæller med i MaxLen og holder en celle, der forbliver tom. Kommer dine kildedata fra et system, der drysser disse mærker ud i navne, så fjern dem før udfyldningen, ellers viser feltet et hul og afviser værdier ét tegn tidligere, end brugeren forventer

Hvorfor skal bidi-omsortering ske, før celler tildeles?

HotPDF kører hele linjen gennem Unicode Bidirectional Algorithm (UAX #9) først og giver først derefter hver scalar sin celle ud fra dens visuelle position. At tildele celler i logisk orden og flippe resultatet bagefter virker for en ren arabisk streng og fejler, i samme øjeblik linjen indeholder cifre eller et latinsk token, for de stykker skal fortsat læses venstre til højre inde i den højre-til-venstre-linje. En værdi som en referencekode med arabisk præfiks og et nummer beholder sine cifre i rigtig orden kun, hvis omsorteringen ser hele linjen på én gang. Oversigten over complex-script text shaping går dybere ned i problemet med logisk versus visuel orden

Comb-celletildeling for en arabisk værdi med cifre i HotPDF til Delphi: logisk orden vender tallet, mens UAX 9 først holder cifrene intakte
kør den bidirektionelle algoritme på hele linjen først, og giv derefter hver scalar cellen ved dens visuelle position, så indlejrede cifre beholder deres orden

Justeringen placerer derefter hele gitteret af brugte celler inde i feltet. Feltets nedarvede /Q-post vinder, når den er til stede; Options.Alignment gælder kun, når formularen ikke har nogen. Det betyder noget for højre-til-venstre-formularer: en kort arabisk værdi i et venstrejusteret comb-felt fylder de venstre bokse, hvilket er spec-korrekt og næsten aldrig, hvad formular-designeren havde tænkt sig. Ret formularens /Q eller sæt htaRight, når feltet ikke har egen quadding. Widgetrotation kommer fra /MK /R-posten, og ved 90 eller 270 grader bytter den logiske tekst-viewport bredde og højde, mens cellelogikken forbliver den samme

At udfylde et mixed-font comb-felt fra Delphi

Mixed-font comb-tekst bruger ét cellegitter, én fontstørrelse og én baseline for hver leveret font. Med FallbackFontFamilies sat til True vælger HotPDF en font til hvert komplet grafem ud fra de leverede familier i deres konfigurerede rækkefølge, så en arabisk face kan bære bogstaverne, mens en latinsk face bærer indlejret ASCII. Hvert fontspan shappes med hele linjen som kontekst, et mærke splittes aldrig fra sin base hen over fonte, og ingen ligatur opfindes hen over en fontgrænse. At lægge hvert span ud med sit eget lokale gitter eller skalere det til sin egen bounding box ville være enklere, og det ville knække både cellepositioner og roterede felter, i samme øjeblik fallback skiftede font midt i linjen

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 til seksten familier
    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;        // allerede default
    Options.FallbackFontFamilies := True;    // default er False
    Options.FontSize := 0;                   // auto-fit, maks. 12
    Options.Alignment := htaRight;           // bruges kun, når /Q mangler

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // skal være forskellig fra SourceFile
  finally
    Doc.Free;
  end;
end;

Bemærk, hvad eksemplet ikke sætter. Under dokumentudfyldning udledes Width, Height, Rotation og CombCells fra hver widgetrektangel, dens /MK-post og feltets MaxLen, så værdier, du lægger i de medlemmer, bliver overskrevet. En FontSize på nul beder om automatisk størrelse, som tjekker hvert clusters udstrækning mod bredden af dets k celler og den fælles metric- og ink-højde af alle deltagende fonte; en fast størrelse klippes blot til widgeten. CharacterSpacing og WordSpacing flytter aldrig comb-positioner, for gitteret alene bestemmer, hvor et cluster lander. HotPDF loader sine HarfBuzz- og FriBidi-runtimes kun, når en værdi faktisk kræver shaping, og værdier uden shaping-behov beholder det eksisterende direkte comb-layout uændret. Til det generelle loaded-PDF-udfyldningsworkflow, inklusiv tilladelser og gemning, se sætning af formularfeltværdier i en loadet PDF

Hvad garanterer et shappet comb-appearance ikke?

Et shappet comb-appearance garanterer korrekt celletildeling, intakte clusters og en bevaret logisk værdi; det garanterer ikke sømløse kursive sammenføjninger eller perfekt tekstekstraktion i hvert værktøj. Begge grænser følger af, hvad et comb-felt er, og du bør kende dem, før du lover en kunde noget

Kursive sammenføjninger hen over celler er den første. Arabiske bogstaver shappes stadig med hel-linje-kontekst, så hvert får sin korrekte initial-, medial- eller finalform, men det faste cellehulrum sætter plads mellem adskilte clusters, og forbindelsesstregene mødes ikke, som de gør i løbende tekst. Det er iboende i at tegne et sammenføjende skriftsystem ind i bokse, og ingen mængde centrering skjuler det

Tekstekstraktion er den anden. Feltets /V beholder den rene logiske værdi, og appearanceens parent Form XObject bærer ét /ActualText-span (ISO 32000-1 §14.9.4) med det komplette input; child Forms bærer ingen. Ekstraktorer, der ærer ActualText, genvinder den oprindelige streng. Ekstraktorer, der ignorerer den, genopbygger tekst ud fra glyph-positioner og kan returnere en anden orden eller duplikere cluster-tekst, for et multi-glyph cluster har ingen enkelt åbenbar Unicode-værdi per glyph. Har et downstream-system brug for værdien, så læs feltet, ikke sideteksten

De resterende grænser er mekaniske. Comb-tekst skal være én enkelt linje uden tabulatorer, og en værdi med flere scalarer end celler fejler i stedet for at blive trunkeret. Fejl er transaktionelle: et manglende glyph, en ugyldig font, en budgetoverskridelse eller en annullering efterlader den forrige værdi og appearances urørte, så en afvist udfyldning aldrig efterlader et halvtegnet felt

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;
// Ved enten fejl beholder feltet sin forrige værdi og sit appearance

Hvor shappede comb-felter passer ind i en formularpipeline

Shappet comb-understøttelse betyder mest for formularer, der allerede findes: selvangivelser, visa-ansøgninger, bank-onboardingpakker og ID-registreringer designet med boksede felter og nu udfyldt på arabisk, hebraisk, persisk eller urdu. Tæl i scalarer, behold den oprindelige værdi i /V, lad ét gitter styre hver font, og test med en værdi, der blander højre-til-venstre-bogstaver, cifre og et combining mark, for den ene streng gennemgår hver regel i denne artikel. Headless-dokument-API'en, comb shaping og font fallback beskrevet her er en del af HotPDF Delphi PDF component