Artykuł techniczny

Pola comb arabskie i hebrajskie w Delphi z HotPDF

HotPDF wypełnia pola comb AcroForm tekstem arabskim, hebrajskim, znakami łączącymi i tekstem o mieszanych kierunkach przez THPDFHeadlessDocument.SetTextFieldValueWithFamilies. Siatka comb jest liczona w skalarach Unicode, ligatura albo litera podstawowa ze swoimi znakami jest centrowana jako jeden klaster na k komórkach, które należą do jej skalarów, a cały wiersz przechodzi przez Unicode Bidirectional Algorithm, zanim jakakolwiek komórka zostanie przydzielona

Każda z tych trzech reguł zastępuje skrót, który wygląda nieźle w łacińskim demo i rozpada się na pierwszym prawdziwym formularzu. Wyobraź sobie wniosek rządowy z polem comb o dziesięciu kratkach na nazwisko zapisane po arabsku. Wypełnij go logiką per znak i dostaniesz litery w formach izolowanych maszerujące od lewej do prawej, ligaturę lam-alef wciśniętą w jedną kratkę, gdy sąsiednia stoi pusta, i fathę unoszącą się nad złą komórką. Każda z tych wad bierze się z liczenia złej jednostki albo porządkowania tekstu w złym momencie

Dwa sposoby wypełnienia arabskiego pola comb w HotPDF w Delphi: skrót per znak miażdży ligaturę lam-alef, wypełnienie ze shapingu centruje każdy klaster
liczenie skalarów Unicode i uporządkowanie wiersza przed przydziałem komórek trzyma ligaturę wycentrowaną na jej dwóch kratkach i nie zostawia żadnej komórki pustej

Czego flaga Comb faktycznie żąda od writera PDF?

Flaga Comb żąda podzielenia pola na dokładnie MaxLen równoodległych pozycji i włożenia jednego znaku w każdą. ISO 32000-1 §12.7.4.3 definiuje Comb jako pozycję bitu 25 w Table 228, flag pól właściwych polom tekstowym, i czyni ją znaczącą tylko wtedy, gdy obecny jest wpis MaxLen z Table 229, a flagi Multiline, Password i FileSelect są wygaszone. Sam bit to $1000000 w 32-bitowej wartości /Ff; jeśli widziałeś kiedyś pola comb zachowujące się dziwnie po podróży przez zmienną zmiennoprzecinkową, artykuł o 32-bitowych flagach pól AcroForm wyjaśnia, dlaczego ten bit ginie pierwszy

Specyfikacja mówi „character" i na tym koniec. Była pisana z myślą o łacińskich kratkach o stałym skoku i nie mówi, czym jest znak, gdy para liter arabskich staje się jednym glifem albo gdy znak samogłoski nie ma własnej szerokości. HotPDF musi wybrać jednostkę i robi to, pilnując surowo reguł strukturalnych: pole comb, które jest jednocześnie multiline albo nie niesie dodatniego MaxLen, jest odrzucane z diagnostyką, a pola hasła nie mogą być polami comb w ogóle

Dlaczego liczyć skalary Unicode, a nie jednostki UTF-16, glify albo grafemy?

HotPDF liczy komórki comb w skalarach Unicode, bo to ta sama jednostka, którą egzekwuje MaxLen, a siatka i kontrola długości muszą się zgadzać, inaczej wartość przechodząca walidację nie zmieści się w swoich kratkach. Trzy kuszące alternatywy łamią tę zgodność każda po swojemu

  • Jednostki kodowe UTF-16 to to, co zwraca Length w Delphi, więc znak uzupełniający jak U+20BB7 zjadłby dwie kratki i dwa sloty budżetu pola
  • Liczniki glifów zależą od fontu: większość fontów arabskich robi z lam plus alef jeden glif, więc liczba komórek zmieniłaby się w chwili zmiany kroju
  • Liczniki grafemów sklejają literę podstawową z jej znakami, więc siatka rozjechałaby się z kontrolą MaxLen liczącą każdy skalar wpisany przez użytkownika

Walidacja MaxLen w THPDFHeadlessDocument przechodzi przez string tylko wtedy, gdy jego długość UTF-16 przekracza limit, i robiąc to, liczy poprawną parę surrogate raz, a samotny surrogate odrzuca wprost. Jeśli twój UI chce ostrzec przed wywołaniem zamiast łapać wyjątek po nim, licz tak samo

Macierz porównująca jednostki UTF-16, glify i grafemy ze skalarami Unicode dla komórek comb HotPDF w Delphi, z lam-alef, beh plus fatha, U+20BB7 i akcentowanym e
tylko kolumna skalarów Unicode zgadza się z kontrolą MaxLen — para surrogate liczy się raz, a liczba komórek nigdy nie zależy od fontu
// Liczy skalary Unicode tak jak siatka comb:
// poprawna para surrogate to jeden skalar
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;

Kilka wartości pokazuje, jak daleko jednostki się rozjeżdżają i dlaczego tylko kolumna skalarów pasuje do tego, na co pole comb budżetuje

Wartośćjednostki UTF-16skalary (komórki)typowe glify
Lam + alef (لا)221 ligatura
Beh + fatha (بَ)222, znak przy podstawie
U+20BB7 (𠮷)211
Decomposed e + U+0301221 albo 2, zależnie od fontu

Jak ligatury i znaki łączące dzielą swoje komórki?

Ukształtowany klaster obejmujący k skalarów zajmuje k sąsiadujących komórek i jest centrowany jako jedna całość na nich. Po shapingu każdy klaster jest mapowany z powrotem na dokładny wycinek tekstu źródłowego, z którego przyszedł, więc HotPDF wie, które skalary — a więc i które komórki — do niego należą. Ligatura lam-alef z przykładu wyżej dostaje dwie kratki i siedzi wycentrowana na obu. Beh z shaddą i fathą to trzy skalary, więc dostaje trzy kratki, ze znakami wciąż przypiętymi do litery

Centrowanie to moment, w którym oczywista implementacja się wykrzacza. Centrowanie każdego glifu we własnej kratce odrywa znak od podstawy, bo pozycja znaku znaczy coś tylko względem litery, którą zdobi. HotPDF trzyma każdy glif wewnątrz klastra przy jego ukształtowanym skoku i offsetach X i Y, potem centruje klaster używając połączonego zakresu poziomego naturalnego skoku, ustawionych originów glifów i faktycznego tuszu glifów. Klaster rusza się jako blok; nic wewnątrz niego nie jest przestawiane

Jedna konsekwencja łatwo umyka. Sterujące kierunkiem jak U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK oraz kontrolki embedding i isolate to skalary Unicode bez glifu. Liczą się do MaxLen i trzymają komórkę, która zostaje pusta. Jeśli twoje dane źródłowe pochodzą z systemu sypiącego tymi znakami po nazwiskach, obetnij je przed wypełnianiem, bo pole pokaże lukę i odrzuci wartości o znak wcześniej, niż użytkownik oczekuje

Dlaczego uporządkowanie bidi musi nastąpić przed przydziałem komórek?

HotPDF przepuszcza cały wiersz przez Unicode Bidirectional Algorithm (UAX #9) najpierw i dopiero potem daje każdemu skalarowi komórkę z jego pozycji wizualnej. Przydzielanie komórek w porządku logicznym i odwracanie wyniku potem działa dla czysto arabskiego stringa i pada, gdy tylko wiersz zawiera cyfry albo łaciński token, bo te biegi muszą zachować czytanie od lewej do prawej wewnątrz wiersza od prawej do lewej. Wartość typu kod referencji z arabskim prefiksem i numerem trzyma cyfry we właściwej kolejności tylko wtedy, gdy uporządkowanie widzi cały wiersz naraz. Przegląd shapingu tekstu skomplikowanych skryptów schodzi głębiej w problem porządku logicznego kontra wizualnego

Przydział komórek comb dla wartości arabskiej z cyframi w HotPDF dla Delphi: porządek logiczny odwraca numer, a UAX 9 najpierw trzyma cyfry nietknięte
przepuść najpierw cały wiersz przez algorytm dwukierunkowy, potem daj każdemu skalarowi komórkę z jego pozycji wizualnej, żeby osadzone cyfry trzymały porządek

Wyrównanie potem pozycjonuje całą siatkę użytych komórek wewnątrz pola. Dziedziczony wpis /Q pola wygrywa, gdy jest obecny; Options.Alignment aplikuje się tylko, gdy formularz go nie ma. To ma znaczenie dla formularzy od prawej do lewej: krótka wartość arabska w polu comb wyrównanym do lewej zapełnia skrajnie lewe kratki, co jest zgodne ze specyfikacją i prawie nigdy nie jest tym, o co chodziło projektantowi formularza. Napraw /Q formularza albo ustaw htaRight, gdy pole nie ma własnego quaddingu. Obrót widgeta pochodzi z wpisu /MK /R, a przy 90 albo 270 stopniach logiczny viewport tekstu zamienia szerokość z wysokością, podczas gdy logika komórek zostaje ta sama

Wypełnianie pola comb o mieszanych fontach z Delphi

Tekst comb o mieszanych fontach używa jednej siatki komórek, jednego rozmiaru fontu i jednej linii bazowej dla każdego podanego fontu. Z FallbackFontFamilies ustawionym na True HotPDF dobiera font dla każdego kompletnego grafemu z podanych rodzin w ich skonfigurowanej kolejności, więc arabski krój może nieść litery, a łaciński osadzone ASCII. Każdy bieg fontowy jest kształtowany z całym wierszem jako kontekstem, znak nigdy nie jest odrywany od podstawy między fontami i żadna ligatura nie jest wymyślana przez granicę fontu. Układanie każdego biegu we własnej lokalnej siatce albo skalowanie go do własnego bounding boxa byłoby prostsze i połamałoby i pozycje komórek, i obrócone pola w chwili, gdy fallback przełączy font w połowie wiersza

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);                  // od jednej do szesnastu rodzin
    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;        // i tak domyślne
    Options.FallbackFontFamilies := True;    // domyślnie False
    Options.FontSize := 0;                   // auto-dopasowanie, limit 12
    Options.Alignment := htaRight;           // używane tylko przy braku /Q

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // musi się różnić od SourceFile
  finally
    Doc.Free;
  end;
end;

Zauważ, czego przykład nie ustawia. Przy wypełnianiu dokumentu Width, Height, Rotation i CombCells są wyprowadzane z każdego prostokąta widgeta, jego wpisu /MK i MaxLen pola, więc wartości, które wstawisz w te pola, są nadpisywane. Zerowe FontSize prosi o automatyczne dobranie rozmiaru, które sprawdza zakres każdego klastra wobec szerokości jego k komórek i wspólnej metryki oraz wysokości tuszu wszystkich uczestniczących fontów; stały rozmiar jest po prostu przycinany do widgeta. CharacterSpacing i WordSpacing nigdy nie ruszają pozycji comb, bo o tym, gdzie ląduje klaster, decyduje sama siatka. HotPDF ładuje swoje runtime HarfBuzz i FriBidi tylko, gdy wartość faktycznie wymaga shapingu, a wartości niepotrzebujące shapingu zachowują istniejący bezpośredni układ comb bez zmian. Ogólny workflow wypełniania wczytanego PDF-a, łącznie z uprawnieniami i zapisem, opisuje ustawianie wartości pól formularza we wczytanym PDF-ie

Czego ukształtowany wygląd comb nie gwarantuje?

Ukształtowany wygląd comb gwarantuje poprawny przydział komórek, nietknięte klastry i zachowaną wartość logiczną; nie gwarantuje bezszwowych połączeń kursywnych ani idealnej ekstrakcji tekstu w każdym narzędziu. Oba limity wynikają z tego, czym pole comb jest, i warto je znać, zanim cokolwiek obiecasz klientowi

Pierwsze są połączenia kursywne przez komórki. Litery arabskie są nadal kształtowane z kontekstem całego wiersza, więc każda dostaje właściwą formę początkową, środkową albo końcową, ale stały skok komórek wkłada odstęp między osobne klastry i kreski łączące nie spotkają się tak, jak w płynnym tekście. To nieodłączna cecha rysowania skryptu łączonego w kratkach i żadne centrowanie tego nie ukryje

Drugą jest ekstrakcja tekstu. /V pola trzyma goły porządek logiczny, a rodzic Form XObject wyglądu niesie jeden bieg /ActualText (ISO 32000-1 §14.9.4) z kompletnym wejściem; formy potomne nie niosą niczego. Ekstraktory honorujące ActualText odzyskają oryginalny string. Ekstraktory je ignorujące składają tekst z pozycji glifów i mogą zwrócić inną kolejność albo zdublowany tekst klastra, bo klaster wieloglifowy nie ma jednej oczywistej wartości Unicode na glif. Jeśli system downstream potrzebuje wartości, czytaj pole, nie tekst strony

Pozostałe granice są mechaniczne. Tekst comb musi być jednym wierszem bez tabulatorów, a wartość o więcej skalarach niż komórek zawodzi zamiast zostać obcięta. Porażki są transakcyjne: brakujący glif, nieważny font, przekroczenie budżetu albo anulowanie zostawiają poprzednią wartość i wygląd nietknięte, więc odrzucone wypełnienie nigdy nie zostawia za sobą pół narysowanego pola

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;
// Przy obu błędach pole zachowuje poprzednią wartość i wygląd

Gdzie ukształtowane pola comb pasują do pipeline'u formularzy

Wsparcie ukształtowanych pól comb znaczy najwięcej dla formularzy, które już istnieją: zeznania podatkowe, wnioski wizowe, zestawy onboardingu bankowego i rejestracje dokumentów tożsamości zaprojektowane z polami w kratkach, teraz wypełniane po arabsku, hebrajsku, persku albo urdu. Licz w skalarach, trzymaj oryginalną wartość w /V, pozwól jednej siatce rządzić każdym fontem i testuj wartością mieszającą litery od prawej do lewej, cyfry i znak łączący, bo ten jeden string ćwiczy każdą regułę z tego artykułu. Headless API dokumentu, shaping comb i fallback fontów opisane tutaj są częścią komponentu HotPDF Delphi PDF