Artykuł techniczny

Kształtowanie tekstu arabskiego i RTL w Delphi z HotPDF

Podaj arabską frazę يوضح ملف PDF do TextOut i otwórz wynik. Litery biegną w złą stronę, a każda z nich siedzi w swojej formie izolowanej z widoczną przerwą przed następną, jakby ktoś wpisał angielski od tyłu i wcisnął spację między każdym znakiem. Żaden wyjątek się nie zgłosił. Żadne ostrzeżenie się nie wypisało. Wyjście jest po prostu błędne, a jest błędne, ponieważ dwie osobne transformacje, od których zależy arabski, nigdy się nie wydarzyły. Wiedza o tym, czym są te dwie transformacje i które wywołanie je wykonuje, to większość tego, do czego sprowadza się wyjście PDF dla pism złożonych

HotPDF jest natywnym komponentem VCL do PDF dla Delphi i C++Builder i wykonuje za ciebie pracę z pismem od prawej do lewej przez odrębne wywołanie. Zatrzymuje się też w kilku konkretnych miejscach, o których warto wiedzieć, zanim zobowiążesz się do jakiegoś locale, więc ten tekst mapuje pojęcia i uczciwe granice; praktyczna konfiguracja samego wywołania mieszka w artykule referencyjnym o RtLTextOut

Dlaczego poprawny łańcuch i tak drukuje się źle

Unicode trzyma tekst w kolejności logicznej, czyli takiej, w jakiej go wpisujesz i czytasz na głos. Renderer musi kłaść glify w kolejności wizualnej. Dla pism od lewej do prawej te kolejności się pokrywają i nikt o tym nie myśli. Dla arabskiego i hebrajskiego już nie, a gdy jeden wiersz miesza kierunki, powiedzmy arabskie zdanie niosące łaciński token „PDF” albo cenę zapisaną cyframi, o tym, jak dokładnie fragmenty od lewej do prawej zagnieżdżają się wewnątrz wiersza od prawej do lewej, decyduje algorytm dwukierunkowy Unicode (UAX #9). To pierwsza transformacja, zmiana kolejności, a jej pominięcie odwraca wiersz

Drugą jest kształtowanie kontekstowe. Litera arabska rysowana jest inaczej w zależności od tego, gdzie wypada w wyrazie: początkowa, środkowa, końcowa albo stojąca samotnie. Punkt kodowy pozostaje przez cały czas ten sam; zmienia się tylko glif. Potok, który podaje każdy punkt kodowy prosto do jego domyślnego glifu, produkuje dokładnie to rozłączone wyjście w formach izolowanych z pierwszego akapitu. Hebrajski pomija ten krok, bo jego litery się nie łączą, ale nadal potrzebuje zmiany kolejności. Arabski potrzebuje obu, i dlatego to arabski, a nie hebrajski, jest łańcuchem, którym testujesz

Na pulpicie nic z tego nie jest twoim problemem. Gdy formularz VCL maluje arabski w TEdit, stos tekstowy systemu operacyjnego po cichu zmienia kolejność i kształtuje, i właśnie dlatego łańcuch wyglądający idealnie na ekranie wychodzi zepsuty w naiwnym PDF. Strumień treści nie przechowuje tekstu edytowalnego. Przechowuje pozycjonowane glify, więc ten, kto emituje strumień, dziedziczy pracę kształtowania, którą kiedyś obsługiwał system. RtLTextOut to wywołanie, które tę pracę odbiera z powrotem

Co RtLTextOut kształtuje za ciebie

HotPDF trzyma ścieżkę łacińską i ścieżkę pism złożonych jako dwie różne metody. TextOut drukuje to, co mu dasz, w kolejności, w jakiej mu to dasz. RtLTextOut wykonuje najpierw obie transformacje — zmianę kolejności dwukierunkową w obrębie całego wiersza, analizę kontekstową dla pism łączących — a potem drukuje. To, których reguł pisma użyć, przyjeżdża przez zestaw znaków czcionki, a nie przez samo wywołanie, więc kierunek jest jawnym wyborem w każdym miejscu wywołania, a nie zgadywaniem na podstawie znaków. Konfiguracja parametr po parametrze, wartości zestawów znaków, kroki rejestracji czcionki i kompletny kompilowalny przykład są w artykule referencyjnym o RtLTextOut; ten tekst zostaje przy tym, co te transformacje znaczą, gdzie się kończą i jak dowieść, że zadziałały

Diagram pokazujący, jak RtLTextOut w HotPDF stosuje zmianę kolejności dwukierunkową i kontekstowe łączenie liter arabskich, zanim glify trafią do strumienia treści PDF w Delphi, w zestawieniu z odwróconym, izolowanym wyjściem naiwnego wywołania TextOut
RtLTextOut wykonuje zmianę kolejności dwukierunkową i łączenie kontekstowe przed rysowaniem, podczas gdy naiwna ścieżka emituje odwrócone, rozłączone litery

Jedna reguła użycia ma znaczenie nawet na tej wysokości: wejście musi być w kolejności logicznej, ponieważ RtLTextOut samo wykonuje odwrócenie, a łańcuch już przez ciebie ręcznie odwrócony wychodzi odwrócony podwójnie — artykuł referencyjny prowadzi przez tę pułapkę i jej sprzątanie. To, co zasługuje tu na wzmiankę, to powód, dla którego ta pułapka przeżywa testy. Podwójnie odwrócony łańcuch czysto arabski potrafi wyglądać całkowicie poprawnie i rozpada się dopiero wtedy, gdy wiersz niesie łacińskie słowo albo liczbę, ponieważ te osadzone przebiegi nie zagnieżdżają się już tak, jak dyktuje UAX #9. Błąd nie tkwi w renderowaniu; tkwi w karmieniu algorytmu tekstem już wpół przetworzonym

To samo zachowanie przy mieszanych kierunkach potyka recenzentów bardziej niż kod. Wewnątrz wiersza od prawej do lewej cyfry i osadzone słowa łacińskie nadal czyta się od lewej do prawej. Ktoś, kto nie pracował z układem dwukierunkowym, spojrzy na wyrenderowaną fakturę, zobaczy numer konta czytający się w „złą” stronę względem otaczającego arabskiego i zgłosi to jako błąd. To wynik zgodny ze specyfikacją. Krótka notatka w twoich kryteriach odbioru, napisana przed pierwszym przejściem native speakera, oszczędza tę podróż w obie strony

Kiedy zmiana kolejności i łączenie wystarczą, a kiedy nie

Dla ciągłego tekstu arabskiego i hebrajskiego — raportów, faktur, umów, listów — zmiana kolejności plus łączenie kontekstowe to cała praca, a RtLTextOut unosi ją samodzielnie. Granica pojawia się, gdy typografia prosi o coś więcej niż łączenie. Odpowiedzią HotPDF po stronie arabskiej jest włączany świadomie shaper po stronie producenta: ustaw AutoShapeArabic := True, a komponent przepisuje przebieg w kolejności logicznej na formy prezentacyjne Unicode przed przejściem dwukierunkowym, więc formy łączące są wyliczane względem sąsiadów logicznych, a zwinięcia ligatur są wypieczone w punktach kodowych, które PDF faktycznie niesie, zamiast zostawiane przeglądarce do rozwiązania. Przełącznik jest domyślnie wyłączony, a wyjście jest stabilne bajtowo, dopóki taki pozostaje, więc włączenie go jest świadomą decyzją dla konkretnego potoku dokumentów, a nie globalną aktualizacją. Ten sam model opcjonalnego włączania rozciąga się na pozostałe łączące pisma od prawej do lewej, które HotPDF kształtuje: syryjskie, N’Ko, adlam i hanifi rohingya mają każde własną flagę automatycznego kształtowania odzwierciedlającą arabską

Opcjonalne funkcje OpenType to znów inny mechanizm. Ligatury uznaniowe i podobne funkcje pojedynczego podstawienia idą przez GetSingleSubstituteGlyph(GID, 'liga'), które rozwiązuje po jednym podstawieniu naraz — najpierw identyfikator glifu wejściowego, potem znacznik funkcji — i zwraca glif wejściowy bez zmian, gdy funkcja nie ma zastosowania. To wystarcza, by obsłużyć znaną, skończoną listę ligatur, którą sam utrzymujesz. To nie jest pełny silnik GSUB, a ta różnica jest dokładnie tym miejscem, w którym ambitne plany na kolejne locale się rozbijają: potok kształtowania obsługujący arabski bezbłędnie udowodnił zmianę kolejności i łączenie, nic więcej

Zasięg pism

Arabski ćwiczy obie transformacje, dlatego jest łańcuchem do testów i dlatego przejście arabskie jest najmocniejszym pojedynczym dowodem, że potok działa. Hebrajski potrzebuje zmiany kolejności, ale nie łączenia, bo jego litery stoją samotnie; jeśli hebrajski renderuje się poprawnie, a arabski wychodzi rozłączony, połowa dwukierunkowa jest w porządku, a połowa kontekstowa nigdy się nie uruchomiła. Perski i urdu jadą na piśmie arabskim i dziedziczą jego zachowanie, choć preferencja urdu dla stylu nastaliq jest decyzją o czcionce mającą konsekwencje dla czytelności, które powinien ocenić rodzimy czytelnik

Tajski siedzi całkowicie po drugiej stronie tej granicy. Biegnie od lewej do prawej, więc nie potrzebuje pracy dwukierunkowej, a jego litery się nie łączą, więc nie potrzebuje analizy kontekstowej; łańcuchy tajskie idą zwykłą ścieżką TextOut jak łacińskie. To, co tajski ma, to piętrzące się znaki — samogłoski i znaki tonu nad i pod spółgłoską bazową — a to, czy siedzą poprawnie, zależy od tego, czy czcionka buduje swoje znaki łączące tak, by piętrzyły się bez pomocy silnika kształtującego. Większość dedykowanych czcionek tajskich to robi. Testuj z dokładnie tą czcionką, którą osadzisz, a nie z podobną

Dewanagari i reszta rodziny indyjskiej to uczciwy twardy koniec. Ich znaki samogłoskowe zmieniają kolejność wokół zbitek spółgłoskowych, a ich ligatury złożone powstają przez łańcuchy podstawień zależnych od kontekstu, co jest terenem pełnego GSUB, poza zmianą kolejności i łączeniem. Jeśli locale indyjskie jest na mapie drogowej, przeprowadź prawdziwy pilotaż na autentycznych łańcuchach klientów, zanim je obiecasz — działający arabski nie jest dowodem, że dewanagari zadziała. Łańcuchy CJK, wietnamski z jego piętrzącymi się znakami diakrytycznymi i mieszany tekst europejski idą zwykłą ścieżką bez analizy dwukierunkowej, a warto trzymać te dwie ścieżki fizycznie osobno w kodzie raportów, jedną procedurę dla przebiegów RTL i jedną dla całej reszty, żeby logika locale była widoczna w miejscu wywołania, a nie ukryta za flagą, o której ktoś zapomni

Mapa decyzyjna zasięgu pism w HotPDF dla Delphi: rodzina arabska wymagająca łączenia i zmiany kolejności, hebrajski wymagający tylko zmiany kolejności, pisma klasy tajskiej od lewej do prawej na zwykłej ścieżce TextOut oraz pisma indyjskie wymagające pełnego silnika GSUB
Każda klasa pisma ćwiczy inny podzbiór potoku, a rodzina indyjska leży poza zmianą kolejności i łączeniem

O zasięgu glifów rozstrzyga się, zanim kształtowanie w ogóle ruszy

Kształtowanie wybiera glify z czcionki. Jeśli czcionka ich nie niesie, nie ma czego wybierać, i dlatego klasyczna awaria wdrożeniowa — bez zarzutu na maszynie programisty, puste prostokąty na serwerze klienta po cichym podstawieniu czcionki — jest problemem zasięgu, a nie kształtowania. Praktyczne lekarstwo, czyli rejestrowanie czcionki, którą sam dostarczasz, zamiast ufania temu, co maszyna ma zainstalowane, jest przeprowadzone krok po kroku w artykule referencyjnym. Pojęciowo chodzi o to, że zasięg trzeba ustalić, zanim jakiekolwiek pytanie o kształtowanie stanie się w ogóle sensowne, i że da się go ustalić programowo, a nie przez oglądanie wyjścia

// Po RegisterUnicodeTTF zbadaj zasięg dla punktów kodowych,
// których twoje dane faktycznie używają
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

Sama rejestracja niesie dwa ograniczenia — dolny próg PDF 1.5 dla osadzonej obsługi Unicode oraz bity uprawnień do osadzania czcionki — oba omówione obok kroków konfiguracji w artykule referencyjnym o RtLTextOut. Tutaj należy nawyk audytu: GetUnicodeGlyphForCodepoint jest twoim systemem wczesnego ostrzegania. Przejdź zakresy punktów kodowych, których twoje dane faktycznie używają, przy starcie usługi i zapisz w logu, jakie identyfikatory glifów wracają. Luka w zasięgu ujawni się wtedy jako linia w logu startowym podczas wdrożenia, a nie jako brakujące znaki na fakturze, która już dotarła do klienta

Kolejność odczytu należy do dokumentu, a nie do glifów

Trafienie w każdy glif zostawia wciąż jedną rzecz niezrobioną. ISO 32000-1 §12.2 definiuje preferencję przeglądarki o nazwie /Direction, która podaje ogólną kolejność odczytu dokumentu. Nie dotyka żadnych glifów. Robi to, że mówi przeglądarce, jak układać rozkładówki po dwie strony, od której strony ma się zaczynać układ sąsiadujących stron i w którą stronę ma się przechylać interfejs czytania. Nic z tego nie widać na pojedynczej stronie, i właśnie dlatego się o tym zapomina

// Zadeklaruj kolejność odczytu od prawej do lewej na poziomie dokumentu
Pdf.Direction := RightToLeft;  // dodaje vpDirection do ViewerPreferences

Ustawienie Direction to cała praca: setter właściwości dodaje vpDirection do ViewerPreferences dokumentu, więc jedna linia wnosi tę preferencję do pliku. Jeśli tekst wychodzi przez RtLTextOut, dostajesz to za darmo, ponieważ wywołanie przestawia kierunek dokumentu jako efekt uboczny — artykuł referencyjny opisuje, kiedy dokument mieszany wymaga cofnięcia tego. Przypadek, w którym musisz ustawić to sam, to dokument od prawej do lewej wyprodukowany w jakikolwiek inny sposób, na przykład z wejścia ukształtowanego wcześniej i narysowanego zwykłą ścieżką. Pomiń to, a jednostronicowy dowód, w który się wpatrujesz, wygląda tak samo w obie strony; potem ktoś drukuje broszurę dwustronną, rozkładówki wychodzą odbite lustrzanie, a przyczyną jest brakująca jedna linia sprzed tygodni

Weryfikacja ukształtowanego wyjścia

Weryfikuj od końca do końca, bo strona może wyglądać poprawnie i wciąż być bezużyteczna dla wszystkiego dalej w łańcuchu. Trzy sprawdzenia znajdują większość problemów. Skopiuj tekst z powrotem z Acrobata i porównaj punkty kodowe z twoim łańcuchem źródłowym. Uruchom wyszukiwanie w dokumencie w przeglądarce dla słowa, które widzisz na stronie. I otwórz wyjście na maszynie, która nie ma twoich czcionek deweloperskich, tej najbardziej skłonnej ujawnić podstawienie. Nic z tego nie zastąpi rodzimego czytelnika patrzącego na jeden prawdziwy dokument, co wyłapuje rzeczy, jakich nie wyłapie żaden syntetyczny korpus. Wpisz ten przegląd do kalendarza, zanim format ruszy w świat

Proces weryfikacji ukształtowanego wyjścia arabskiego i RTL z HotPDF w Delphi: porównanie punktów kodowych w obie strony, wyszukiwanie w dokumencie, sprawdzenie czcionek na obcej maszynie i przejście rodzimego czytelnika przez minimalny zestaw testowy locale
Weryfikacja od końca do końca łączy trzy sprawdzenia maszynowe z jednym przeglądem native speakera przed wypuszczeniem locale

Dobieraj łańcuchy testowe świadomie, zamiast odgrzewać to, co tłumacz przysłał rok temu. Działające minimum na locale: zdanie w czystym piśmie, zdanie z osadzonymi łacińskimi nazwami marek, wiersz niosący cyfry i walutę oraz nazwiska ze znakami diakrytycznymi lub łączącymi. Prawdziwe nazwiska klientów łamią założenia, których tekst wypełniający nie tyka, więc pozwól zestawowi regresyjnemu rosnąć o jeden łańcuch za każdym razem, gdy zgłoszenie serwisowe ujawni wzorzec, którego nie widziałeś

Rejestrację czcionek, tworzenie podzbiorów i codzienne API rysowania tekstu omawia artykuł o wyjściu raportów, czcionkach i obrazach z HotPDF. Gdy te same dokumenty muszą też spełniać profile dostępności, reguły znakowania języka i struktury z artykułu o walidacji PDF/A i PDF/UA kładą się na opisanej tu pracy kształtowania

Opisane wyżej API od prawej do lewej i API czcionek Unicode są dostarczane z komponentem HotPDF dla Delphi oraz C++Builder; strona produktu linkuje pełne odniesienie do wyjścia tekstowego