Teknisk artikel

Arabiska och hebreiska comb-fält i Delphi med HotPDF

HotPDF fyller AcroForm comb-fält med arabiska, hebreiska, kombinerande märken och blandad-riktning-text via THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Comb-rutnätet räknas i Unicode-scalars, en ligatur eller en basbokstav med sina märken centreras som ett kluster över de k celler dess scalars äger, och hela raden kör igenom Unicode Bidirectional Algorithm innan någon cell tilldelas

Var och en av de tre reglerna ersätter en genväg som ser fin ut i en latinsk demo och faller sönder på det första riktiga formuläret. Föreställ dig en myndighetsansökan med ett comb-fält i tio rutor för ett namn skrivet på arabiska. Fyll det med per-tecken-logik och du får bokstäver i isolerade former som marscherar vänster till höger, en lam-alef-ligatur pressad i en ruta medan dess granne står tom, och en fatha som svävar över fel cell. Vart och ett av de felen kommer från att räkna fel enhet eller ordna texten vid fel ögonblick

Två sätt att fylla ett arabiskt comb-fält med HotPDF i Delphi: per-teckengenvägen krossar lam-alef-ligaturen, den formade fyllningen centrerar varje kluster
att räkna Unicode-scalars och ordna om raden innan celler tilldelas håller ligaturen centrerad över sina två rutor och lämnar ingen cell tom

Vad berättar Comb-flaggan egentligen för en PDF-skrivare?

Comb-flaggan ber skrivaren dela fältet i exakt MaxLen lika fördelade positioner och sätta ett tecken i varje. ISO 32000-1 §12.7.4.3 definierar Comb som bitposition 25 i Table 228, fältflaggorna specifika för textfält, och gör den meningsfull bara när MaxLen-posten från Table 229 finns och Multiline-, Password- och FileSelect-flaggorna är rensade. Biten själv är $1000000 i det 32-bitars /Ff-värdet; har du någonsin sett comb-fält bete sig konstigt efter en roundtrip genom en flyttalsvariabel förklarar artikeln om 32-bitars AcroForm-fältflaggor varför just den biten går först

Specifikationen säger ”tecken” och stannar där. Den skrevs med latinska boxar i fast bredd i åtanke och säger inte vad ett tecken är när ett arabiskt bokstavspar blir en glyph, eller när ett vokalmärke inte har egen bredd. HotPDF måste välja en enhet, och den driver de strukturella reglerna strikt medan det sker: ett comb-fält som också är multiline, eller som bär ingen positiv MaxLen, avvisas med en diagnos, och lösenordsfält kan inte vara comb-fält alls

Varför räkna Unicode-scalars i stället för UTF-16-enheter, glypher eller grafemer?

HotPDF räknar comb-celler i Unicode-scalars för det är samma enhet den använder för att driva MaxLen, och rutnätet och längdkontrollen måste vara överens annars passar inte ett värde som klarar valideringen sina rutor. De tre frestande alternativen bryter var och en den överenskommelsen på olika sätt

  • UTF-16-kodenheter är vad Delphis Length returnerar, så ett supplerande tecken som U+20BB7 skulle äta två rutor och två platser av fältbudgeten
  • Glyph-antal beror på fonten: de flesta arabiska fonts gör lam plus alef till en glyph, så cellantalet skulle ändras i samma stund du byter font
  • Grafem-antal slår ihop en basbokstav med sina märken, vilket betyder att rutnätet skulle säga emot en MaxLen-kontroll som räknar varje scalar användaren skrev

MaxLen-valideringen i THPDFHeadlessDocument går bara igenom strängen när dess UTF-16-längd överskrider gränsen, och när den gör det räknar den ett giltigt surrogatpar en gång och avvisar ett oparat surrogat rakt av. Vill ditt UI varna före anropet i stället för att fånga ett exception efteråt, räkna på samma sätt

Matris som jämför UTF-16-enheter, glypher och grafemer mot Unicode-scalars för HotPDF comb-celler i Delphi, med lam-alef, beh plus fatha, U+20BB7 och accentuerat e
bara Unicode-scalar-kolumnen stämmer med MaxLen-kontrollen — ett surrogatpar räknas en gång och cellantalet beror aldrig på fonten
// Räknar Unicode-scalars som comb-rutnätet gör:
// ett giltigt surrogatpar är en 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;

Några värden visar hur långt enheterna glider isär, och varför bara scalar-kolumnen stämmer med vad ett comb-fält budgeterar för

VärdeUTF-16-enheterScalars (celler)Typiska glypher
Lam + alef (لا)221 ligatur
Beh + fatha (بَ)222, märke fäst vid basen
U+20BB7 (𠮷)211
Decomposed e + U+0301221 eller 2, fontberoende

Hur delar ligaturer och kombinerande märken sina celler?

Ett format kluster som täcker k scalars upptar k sammanhängande celler och centreras som en enda enhet över dem. Efter shaping mappas varje kluster tillbaka till den exakta spännvidden av källtext den kom från, så HotPDF vet vilka scalars, och alltså vilka celler, den äger. Lam-alef-ligaturen ovan får två rutor och sitter centrerad över båda. En beh som bär en shadda och en fatha är tre scalars, så den får tre rutor, med märkena fortfarande fästa vid bokstaven

Centreringen är där den uppenbara implementationen går snett. Att centrera varje glyph i sin egen ruta drar ett märke från sin bas, för märkets position betyder bara något i förhållande till bokstaven den smyckar. HotPDF behåller varje glyph inuti ett kluster på sin formade advans och sina X- och Y-offset, och centrerar sedan klustret med den kombinerade horisontella utsträckningen av den naturliga advansen, de positionerade glyph-origo och glyphens faktiska bläck. Klustret flyttar sig som ett block; ingenting inuti det arrangeras om

En konsekvens är lätt att missa. Riktningskontroller som U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK, och embedding- och isolate-kontrollerna är Unicode-scalars utan glyph. De räknas mot MaxLen och håller en cell som förblir tom. Kommer din källdata från ett system som strör de här märkena in i namn, ta bort dem före fyllningen, annars visar fältet ett gap och avvisar värden ett tecken tidigare än användaren väntar sig

Varför måste bidi-omordning ske innan celler tilldelas?

HotPDF kör hela raden genom Unicode Bidirectional Algorithm (UAX #9) först, och först därefter ger varje scalar sin cell från sin visuella position. Att tilldela celler i logisk ordning och vända resultatet efteråt funkar för en ren arabisk sträng och faller så snart raden innehåller siffror eller en latinsk token, för de körningarna måste fortsätta läsas vänster till höger inuti höger-till-vänster-raden. Ett värde som en referenskod med arabiskt prefix och ett nummer håller sina siffror i rätt ordning bara om omordningen ser hela raden på en gång. Översikten om complex-script textshaping täcker problemet logisk-mot-visuell ordning mer i djupet

Comb-cellstilldelning för ett arabiskt värde med siffror i HotPDF för Delphi: logisk ordning vänder numret, medan UAX 9 först håller siffrorna intakta
kör den bidirektionella algoritmen på hela raden först, och ge sedan varje scalar cellen vid dess visuella position så inbäddade siffror behåller sin ordning

Justering positionerar sedan hela rutnätet av använda celler inuti fältet. Fältets ärvda /Q-post vinner när den finns; Options.Alignment gäller bara när formuläret saknar en. Det spelar roll för höger-till-vänster-formulär: ett kort arabiskt värde i ett vänsterjusterat comb-fält fyller de vänstraste rutorna, vilket är spec-korrekt och nästan aldrig vad formulärdesignern avsåg. Fixa formulärets /Q, eller sätt htaRight när fältet saknar egen quadding. Widget-rotation kommer från /MK-posten /R, och vid 90 eller 270 grader byter den logiska textvisningsytan plats på bredd och höjd medan celllogiken förblir densamma

Fylla ett blandad-font comb-fält från Delphi

Blandad-font comb-text använder ett cellrutnät, en fontstorlek och en baslinje för varje levererad font. Med FallbackFontFamilies satt till True plockar HotPDF en font för varje komplett grafem ur de levererade familjerna i deras konfigurerade ordning, så ett arabiskt ansikte kan bära bokstäverna medan ett latinskt ansikte bär inbäddad ASCII. Varje fontspan formeras med hela raden som kontext, ett märke delas aldrig från sin bas över fonts, och ingen ligatur hittas på över en fontgräns. Att lägga ut varje span med sitt eget lokala rutnät eller skala det till sin egen bounding box vore enklare, och det skulle krossa både cellpositioner och roterade fält i samma stund fallbacken bytte font mitt på raden

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);                  // en till sexton familjer
    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;        // redan standard
    Options.FallbackFontFamilies := True;    // standard är False
    Options.FontSize := 0;                   // autoanpassning, tak 12
    Options.Alignment := htaRight;           // används bara när /Q saknas

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // måste skilja sig från SourceFile
  finally
    Doc.Free;
  end;
end;

Notera vad exemplet inte sätter. Under dokumentfyllning härleds Width, Height, Rotation och CombCells från varje widgetrektangel, dess /MK-post och fältets MaxLen, så värden du stoppar in i de medlemmarna skrivs över. En FontSize noll ber om automatisk storlek, som kontrollerar varje klusters utsträckning mot bredden av dess k celler och den delade metriken och bläckhöjden hos alla deltagande fonts; en fast storlek klipps helt enkelt till widgeten. CharacterSpacing och WordSpacing flyttar aldrig comb-positioner, eftersom rutnätet ensamt avgör var ett kluster landar. HotPDF laddar sina HarfBuzz- och FriBidi-runtimes bara när ett värde faktiskt kräver shaping, och värden som inte behöver shaping behåller den befintliga direkta comb-layouten oförändrad. För det allmänna fyllningsarbetsflödet i laddad PDF, inklusive behörigheter och sparning, se att sätta formulärfältvärden i en laddad PDF

Vad kan du inte räkna med från ett format comb-utseende?

Ett format comb-utseende garanterar korrekt cellstilldelning, intakta kluster och ett bevarat logiskt värde; det garanterar inte sömlösa kursiva övergångar eller perfekt textextrahering i varje verktyg. Båda gränserna följer av vad ett comb-fält är, och du bör känna till dem innan du utlovar något till en kund

Kursiva övergångar över celler är den första. Arabiska bokstäver formas fortfarande med helrads-kontext, så var och en får sin korrekta initial-, medial- eller finalform, men det fasta cellsteget lägger mellanrum mellan separata kluster och förbindelsestrecken möts inte som de gör i löpande text. Det är inneboende i att rita ett förbindande skriftsystem i boxar, och ingen mängd centrering döljer det

Textextrahering är den andra. Fältets /V behåller det rena logiska värdet, och utseendets förälder Form XObject bär en /ActualText-span (ISO 32000-1 §14.9.4) med hela inmatningen; barn-Formsen bär ingen. Extraktorer som hedrar ActualText återvinner originalsträngen. Extraktorer som ignorerar den bygger texten på nytt från glyph-positioner och kan returnera en annan ordning eller duplicera klustertext, för ett kluster med flera glypher har inget enda uppenbart Unicode-värde per glyph. Behöver ett nedströmssystem värdet, läs fältet, inte sidtexten

De kvarvarande gränserna är mekaniska. Comb-text måste vara en enda rad utan tabbar, och ett värde med fler scalars än celler faller i stället för att trunkeras. Fel är transaktionella: en saknad glyph, en ogiltig font, en budgetöverskridning eller en avbrytning lämnar det tidigare värdet och utseendena orörda, så en avvisad fyllning lämnar aldrig ett halvritat fält efter sig

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;
// Vid något av felen behåller fältet sitt tidigare värde och utseende

Var formade comb-fält passar in i en formulärpipeline

Stöd för formade comb-fält spelar störst roll för formulär som redan finns: deklarationer, visumansökningar, bank-onboarding-paket och ID-registreringar designade med boxade fält och nu fyllda på arabiska, hebreiska, persiska eller urdu. Räkna i scalars, behålla originalvärdet i /V, låt ett rutnät styra varje font, och testa med ett värde som blandar höger-till-vänster-bokstäver, siffror och ett kombinerande märke, för den ensamma strängen tränar varje regel i den här artikeln. Headless-dokument-API:et, comb-shaping och font-fallback som beskrivs här ingår i HotPDF Delphi PDF-komponenten