Technický článek

Arabská a hebrejská comb pole v Delphi s HotPDF

HotPDF vyplňuje AcroForm comb pole arabským a hebrejským textem, combining marks a smíšenými směry přes THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Comb mřížka se počítá v Unicode scalars, ligatura nebo základní písmeno se svými markami se centrují jako jeden cluster napříč k buňkám, které jejich scalars vlastní, a celý řádek projde Unicode Bidirectional Algorithm, ještě než se přidělí jediná buňka

Každé z těch tří pravidel nahrazuje zkratku, která v latinské ukázce vypadá dobře a na prvním reálném formuláři se rozpadne. Představte si vládní přihlášku s comb polem o deseti boxech na jméno psané arabsky. Vyplníte ji logikou po znacích a dostanete písmena v izolovaných tvarech pochodující zleva doprava, ligaturu lam-alef vestlanou do jednoho boxu, zatímco soused zůstává prázdný, a fathu vlající nad špatnou buňkou. Každou z těch chyb způsobí počítání špatné jednotky nebo řazení textu ve špatný okamžik

Dva způsoby vyplnění arabského comb pole v HotPDF v Delphi: zkratka po znacích rozdrtí ligaturu lam-alef, shaped vyplnění centrů každý cluster
počítání Unicode scalars a přeřazení řádku před přidělením buněk drží ligaturu centrovanou přes její dva boxy a nezanechává prázdnou buňku

Co vlastně příznak Comb žádá po PDF writeru?

Příznak Comb žádá writer, aby pole rozdělil na přesně MaxLen stejně rozestavěných pozic a do každé uložil po jednom znaku. ISO 32000-1 §12.7.4.3 definuje Comb jako bitovou pozici 25 v Table 228, příznacích polí specifických pro textová pole, a dává smysl jen tehdy, když existuje položka MaxLen z Table 229 a příznaky Multiline, Password a FileSelect jsou vypnuté. Bit sám je $1000000 v 32bitové hodnotě /Ff; pokud jste někdy viděli comb pole blbnout po cestě přes proměnnou s plovoucí čárkou, článek o 32bitových AcroForm příznacích polí vysvětluje, proč tenhle bit mizí jako první

Specifikace říká „character" a končí. Psala se s latinskými boxy pevné šířky v hlavě a neříká, co je znak, když se dvojice arabských písmen stane jedinou glyfou nebo když samohlásková marka nemá žádnou vlastní šířku. HotPDF si musí vybrat jednotku a strukturní pravidla přitom vynucuje přísně: comb pole, které je zároveň multiline, nebo nemá kladné MaxLen, se odmítne s diagnostikou a password pole nesmí být comb polem vůbec

Proč počítat Unicode scalars místo UTF-16 jednotek, glyfů nebo graphemes?

HotPDF počítá comb buňky v Unicode scalars, protože to je tatáž jednotka, kterou vynucuje MaxLen, a mřížka s kontrolou délky se musí shodovat, jinak se hodnota, která projde validací, do svých boxů nevejde. Tři lákavé alternativy kazí tu shodu každá jinak

  • UTF-16 code units je to, co vrací Delphi Length, takže supplementární znak jako U+20BB7 by snědl dva boxy a dvě pozice rozpočtu pole
  • Počty glyfů závisí na fontu: většina arabských fontů dělá z lam plus alef jedinou glyfu, takže počet buněk by se změnil v okamžiku, kdy vyměníte písmo
  • Počty graphemes slévají základní písmeno s jeho markami, takže mřížka by se rozcházela s kontrolou MaxLen počítající každý scalar, který uživatel napsal

Validace MaxLen v THPDFHeadlessDocument prochází řetězec jen tehdy, když jeho UTF-16 délka limit přesahuje, a když ano, platný surrogate pair počítá jednou a nepárovaný surrogate odmítá rovnou. Pokud chce vaše UI varovat před voláním místo chytání výjimky po něm, počítejte stejně

Matice srovnávající UTF-16 jednotky, glyfy a graphemes proti Unicode scalars pro comb buňky HotPDF v Delphi, s lam-alef, beh plus fatha, U+20BB7 a e s diakritikou
se sloupcem Unicode scalars souhlasí kontrola MaxLen jediná — surrogate pair se počítá jednou a počet buněk nikdy nezávisí na fontu
// Počítá Unicode scalars stejně, jako to dělá comb mřížka:
// platný surrogate pair je jeden 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;

Pár hodnot ukazuje, jak daleko se jednotky rozejdou a proč sloupci scalars jediné sedí to, na co comb pole dává rozpočet

HodnotaUTF-16 jednotkyScalars (buňky)Typické glyfy
Lam + alef (لا)221 ligatura
Beh + fatha (بَ)222, marka přichycená k základnímu písmenu
U+20BB7 (𠮷)211
Decomposed e + U+0301221 nebo 2, podle fontu

Jak se ligatury a combining marks dělí o své buňky?

Shaped cluster pokrývající k scalars obsadí k sousedících buněk a centruje se jako jeden celek napříč nimi. Po shapingu se každý cluster zpětně namapuje na přesný úsek zdrojového textu, ze kterého vzešel, takže HotPDF ví, které scalars a tedy které buňky vlastní. Ligatura lam-alef výše dostane dva boxy a sedí centrovaná přes oba. Beh nesoucí shaddu a fathu je tři scalars, takže dostane tři boxy, přičemž marky zůstávají přichycené k písmenu

Centrování je místo, kde se zjevná implementace splete. Centrovení každé glyfy v jejím vlastním boxu odtrhne marku od základu, protože pozice marky má smysl jen relativně k písmenu, které zdobí. HotPDF nechává každou glyfu uvnitř clusteru na jejím shaped advance a jejích offsetech X a Y a pak cluster centruje podle kombinovaného horizontálního rozsahu přirozeného advancu, pozicovaných počátků glyf a skutečné inkoustové plochy glyf. Cluster se pohybuje v celku; nic uvnitř něj se nepřeskládává

Jeden důsledek jde snadno přehlédnout. Směrové řídicí znaky jako U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK a embedding a isolate kontrolní znaky jsou Unicode scalars bez glyfy. Počítají se do MaxLen a drží si buňku, která zůstane prázdná. Pokud vaše zdrojová data přicházejí ze systému, který tyhle marky do jmen sype, odstraňte je před vyplněním, jinak pole ukáže mezeru a hodnoty odmítne o znak dřív, než uživatel čeká

Proč musí přeřazení bidi proběhnout před přidělením buněk?

HotPDF pustí celý řádek nejdřív Unicode Bidirectional Algorithmem (UAX #9) a teprve potom dá každému scalaru jeho buňku podle vizuální pozice. Přidělování buněk v logickém pořadí a překlopení výsledku potom funguje u čistě arabského řetězce a selže, jakmile řádek obsahuje číslice nebo latinský token, protože tyhle úseky si uvnitř pravolevého řádku musí udržet čtení zleva doprava. Hodnota jako referenční kód s arabským prefixem a číslem drží číslice ve správném pořadí jen tehdy, když přeřazení vidí celý řádek najednou. Přehled shapingu komplexních písem rozebírá problém logického verzus vizuálního pořadí do hloubky

Přidělování comb buněk arabské hodnotě s číslicemi v HotPDF pro Delphi: logické pořadí obrátí číslo, zatímco UAX 9 první drží číslice vcelku
pusťte bidirekční algoritmus nejprve na celý řádek a pak každému scalaru přidělujte buňku na jeho vizuální pozici, aby vložené číslice držely pořadí

Zarovnání pak umístí celou mřížku použitých buněk dovnitř pole. Zděděná položka /Q pole vyhrává, když je přítomná; Options.Alignment se aplikuje jen tehdy, když ji formulář nemá. To má význam u formulářů psaných zprava doleva: krátká arabská hodnota v comb poli zarovnaném doleva zaplní nejlevější boxy, což je podle specifikace správně a skoro nikdy to není to, co autor formuláře zamýšlel. Opravte /Q formuláře nebo nastavte htaRight, když pole žádné quadding nemá. Rotace widgetu pochází z položky /MK /R a při 90 nebo 270 stupních si logický textový viewport vymění šířku s výškou, zatímco logika buněk zůstává stejná

Vyplnění comb pole se smíšenými fonty z Delphi

Comb text se smíšenými fonty používá jednu mřížku buněk, jednu velikost písma a jednu baseline pro každý dodaný font. S FallbackFontFamilies nastaveným na True vybírá HotPDF font pro každý kompletní grapheme z dodaných family v nakonfigurovaném pořadí, takže arabský řez nese písmena a latinský řez vložené ASCII. Každý fontový úsek se shapeuje s celým řádkem jako kontextem, marka se nikdy neoddělí od svého základu napříč fonty a ligatura se přes hranici fontů nevymýšlí. Rozvraset každý úsek vlastní lokální mřížkou nebo ho škálovat do vlastního bounding boxu by bylo jednodušší a rozbilo by to pozice buněk i otočená pole v okamžiku, kdy fallback přepne fonty uprostřed řádku

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);                  // jedna až šestnáct family
    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;        // už je default
    Options.FallbackFontFamilies := True;    // default je False
    Options.FontSize := 0;                   // auto-fit, strop na 12
    Options.Alignment := htaRight;           // použije se jen bez /Q

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // musí se lišit od SourceFile
  finally
    Doc.Free;
  end;
end;

Všimněte si, co příklad nenastavuje. Během plnění dokumentu se Width, Height, Rotation a CombCells odvozují z obdélníku každého widgetu, jeho položky /MK a MaxLen pole, takže hodnoty, které do těch členů napíšete, se přepíšou. Nulové FontSize žádá automatické rozměrování, které kontroluje rozsah každého clusteru proti šířce jeho k buněk a sdílené metrické a inkoustové výšce všech zúčastněných fontů; pevná velikost se jednoduše ořízne na widget. CharacterSpacing a WordSpacing comb pozice nikdy nepohnou, protože o tom, kde cluster dopadne, rozhoduje sama mřížka. HotPDF nahraje své runtimes HarfBuzz a FriBidi jen tehdy, když hodnota skutečně potřebuje shaping, a hodnoty, které shaping nepotřebují, si nechávají stávající přímý comb layout beze změny. Obecný workflow plnění načteného PDF, včetně oprávnění a ukládání, popisuje nastavování hodnot formulářových polí v načteném PDF

Co shaped podoba comb pole negarantuje?

Shaped podoba comb pole garantuje správné přidělení buněk, neporušené clustery a zachovanou logickou hodnotu; negarantuje plynulé kurzivní spoje ani dokonalou extrakci textu v každém nástroji. Obě omezení vyplývají z toho, co comb pole je, a měli byste je znát, než zákazníkovi slíbíte cokoliv

První jsou kurzivní spoje napříč buňkami. Arabská písmena se stále shapeují s kontextem celého řádku, takže každé dostane svůj správný tvar initial, medial nebo final, ale pevný rozestup buněk vloží mezeru mezi oddělené clustery a spojovací tahy se nepotkají tak, jak se potkávají v plynulém textu. To je vlastní kreslení spojovacího písma do boxů a žádné centrovení to nezakryje

Druhá je extrakce textu. Položka /V pole drží prostou logickou hodnotu a rodičovský Form XObject podoby nese jediný úsek /ActualText (ISO 32000-1 §14.9.4) s kompletním vstupem; podřízené Forms žádný nenosí. Extrakátory, které ActualText respektují, obnoví původní řetězec. Extrakátory, které ho ignorují, staví text znovu z pozic glyf a mohou vrátit jiné pořadí nebo zdvojený text clusteru, protože multi-glyph cluster nemá pro každý glyph jedinou zjevnou Unicode hodnotu. Pokud hodnotu potřebuje systém po proudu, čtěte pole, ne text stránky

Zbývající hranice jsou mechanické. Comb text musí být jediný řádek bez tabulátorů a hodnota s víc scalars než buněk selže místo ořezání. Selhání jsou transakční: chybějící glyfa, neplatný font, překročený rozpočet nebo zrušení nechá předchozí hodnotu a podoby nedotčené, takže odmítnuté vyplnění nikdy nezanechá napůl nakreslené pole

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;
// V obou chybových případech si pole nechává předchozí hodnotu a podobu

Kam shaped comb pole patří ve formulářové pipeline

Podpora shaped comb polí nejužitečnější je u formulářů, které už existují: daňová přiznání, vízové přihlášky, onboarding balíčky bank a registrace identit navržené s boxovanými poli a dnes vyplňované arabsky, hebrejsky, persky nebo urdusky. Počítejte ve scalars, držte původní hodnotu v /V, nechte jednu mřížku vládnout všem fontům a testujte hodnotou, která míchá pravolevá písmena, číslice a combining marku, protože jediný takový řetězec protáhne každé pravidlo z tohohle článku. Headless document API, comb shaping a font fallback popsané tady jsou součástí HotPDF Delphi PDF component