Kształtowanie tekstu w komponencie PDFium przechodzi przez jeden instalowalny obiekt. ConfigureTextShaper instaluje kształtownik, przez który kieruje się każde wejście kształtowania, zastępując i zwalniając to, co tam było; ActiveTextShaper zwraca zainstalowany i tworzy domyślny platformowy przy pierwszym użyciu; ActiveTextShaperName zgłasza, który backend jest żywy; ClearTextShaper zrzuca instalację i pozwala domyślnemu zostać utworzonym ponownie. Na Windows domyślnym jest TPdfUniscribeTextShaper. Pod Free Pascalem jest TPdfHarfBuzzTextShaper, który wiąże libharfbuzz w czasie wykonania, więc brakująca biblioteka jest zgłaszanym stanem, a nie awarią ładowania
Jeden interfejs, dwa backendy dzielące pracę zupełnie inaczej. Zrozumienie tej asymetrii to to, co powstrzymuje ścieżkę przenośną przed produkcją tekstu poprawnie ukształtowanego i błędnie pozycjonowanego
Czemu backend Windows to jedna klasa, a przenośny trzy kawałki?
Bo Uniscribe to cztery API udające jedno. ScriptItemize segmentuje łańcuch po skryptach i rozwiązuje poziomy dwukierunkowe; ScriptShape mapuje znaki na glify; ScriptPlace liczy przesunięcia i offsety; ScriptLayout układa powstałe przebiegi w kolejność wizualną. Backend na nim zbudowany nie ma więc już nic do dodania, dlatego kształtownik Windows to pojedyncza klasa z pojedynczą metodą
HarfBuzz pokrywa środkowe dwa. Kształtuje i pozycjonuje przebieg, którego kierunek i skrypt wywołujący już zadecydował, i nie ma zdania o tym, jak akapit dzieli się na przebiegi ani w jakiej kolejności te przebiegi występują. Przenośny backend dostarcza więc resztę: algorytm dwukierunkowy rozwiązuje poziomy zagnieżdżenia, funkcje Unicode HarfBuzza segmentują tekst po skryptach, a przebiegi są układane w kolejność wizualną, którą produkuje reguła L2 z UAX #9. Połowa dwukierunkowa jest na tyle znacząca, że stanowi własną jednostkę, opisaną w artykule o poziomach zagnieżdżenia UAX #9
Kształtownik nie rozwiązuje fontów i to jest zamierzone
Uniscribe czyta binarium fontu z kontekstu urządzenia GDI. Nie ma przenośnego odpowiednika tego, a wymyślenie go wewnątrz jednostki kształtowania znaczyłoby decydowanie w imię każdej aplikacji, czy fonty pochodzą z fontconfig, z CoreText, z folderu fontów aplikacji czy z bazy danych. Backend HarfBuzza bierze więc resolver: callback mapujący nazwę fontu na bajty TrueType albo OpenType. Zwrócenie False zawodzi żądanie kształtowania tak samo, jak zawodzi je nieczytelny font GDI na Windows
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// Twoja polityka: fontconfig, CoreText, folder fontów aplikacji, baza danych
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// Własność przechodzi na jednostkę; wywołaj raz podczas startu,
// zanim cokolwiek ukształtuje tekst
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// Na Delphi domyślny platformowy (Uniscribe) jest tworzony na żądanie,
// więc instalacja nie jest w ogóle potrzebna
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
Trzymanie odkrywania fontów poza kształtownikiem ma drugą korzyść ujawniającą się na serwerach: ten sam proces może kształtować zestawem fontów osadzonych, który nie ma nic do rzeczy z tym, co jest zainstalowane na maszynie, i to jest to, czego chcesz, gdy wyjście musi być odtwarzalne bajt w bajt między gospodarzami. Komponent wystawia też dostawcę fontów systemu gospodarza dla przypadków, gdy jednak chcesz fontów zainstalowanych, omówiony w artykule o dostawcy fontów systemowych
Rekord wyniku jest neutralny wobec backendów i klastry są powodem
Oba backendy wypełniają ten sam TPdfShapedText: tekst źródłowy, nazwę fontu, rozmiar, bajty fontu, tablicę przebiegów, całkowitą szerokość, liczbę glifów i liczbę znaków logicznych. Każdy TPdfShapedRun niesie swój wycinek tekstu źródłowego, wizualną pozycję X, szerokość, poziom dwukierunkowy i flagę od prawej do lewej, plus swoje glify. Każdy TPdfShapedGlyph niesie identyfikator glifa, przesunięcie, offsety X i Y oraz klaster, do którego należy, jako początek i długość w tekście źródłowym
Te pola klastrów są tym, co czyni rekord używalnym, a nie jedynie informacyjnym. Kształtowanie nie jest mapowaniem jeden do jednego: sylaba dewanagari staje się jednym glifem z czterech znaków, arabska ligatura scala dwa, a pojedynczy znak może wyprodukować kilka znaków diakrytycznych. Bez wycinków klastrów nie umieszczajesz karetki, nie testujesz trafienia kliknięcia ani nie wyróżniasz zaznaczenia, bo nie potrafisz powiedzieć, do których znaków glif należy. Z nimi arytmetyka jest lokalna i ten sam kod działa dla obu backendów
var
Shaped: TPdfShapedText;
R, G: Integer;
begin
if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
for R := 0 to High(Shaped.Runs) do
begin
// Przebiegi przychodzą już w kolejności wizualnej z wypełnionym VisualX
X := Shaped.Runs[R].VisualX;
for G := 0 to High(Shaped.Runs[R].Glyphs) do
begin
EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
X + Shaped.Runs[R].Glyphs[G].OffsetX,
Shaped.Runs[R].Glyphs[G].OffsetY);
X := X + Shaped.Runs[R].Glyphs[G].Advance;
end;
end;
end;
Budżety należą do rekordu opcji
TPdfTextShapingOptions niesie kierunek plus trzy sufity: maksimum znaków, maksimum glifów i maksimum przebiegów, z klasową funkcją Default wypełniającą rozsądne wartości. Sufity to nie paranoja wobec zdeformowanego wejścia; to arytmetyka. Kształtowanie rozszerza: font z agresywną substitucją kontekstową może wyemitować więcej glifów niż znaków wejściowych, a akapit naprzemiennie zmieniający skrypty co kilka znaków produkuje przebieg na każde przełączenie. Dokument złożony, by zmaksymalizować jedno i drugie, zamienia skromny łańcuch w dużą alokację, a usługa kształtująca tekst z niezaufanych PDF-ów potrzebuje limitu, który wybrała, a nie limitu, który narzuca maszyna
Ustawianie kierunku jawnie, zamiast zostawiania go na automatycznym, jest warte zrobienia zawsze, gdy już go znasz. Automatyczny stosuje reguły kierunku akapitu, by zgadywać z pierwszego mocnego znaku, co jest właściwe dla tekstu wolnego i złe dla pola formularza, którego kierunek jest własnością pola, a nie wartości wpisanej przez kogoś
Wiązanie w czasie wykonania, nie zależność budowania
Backend HarfBuzza ładuje bibliotekę dynamicznie. To decyzja wdrożeniowa z realnymi konsekwencjami: jeden plik binarny działa na maszynie z HarfBuzz i na maszynie bez niego, zgłaszając zredukowaną zdolność w drugim przypadku zamiast zawieść start. Dla biblioteki dostarczanej innym programistom to jedyne działające ustawienie, bo nie możesz wymagać od każdego konsumenta komponentu PDF pozyskiwania i dopasowywania wersji biblioteki kształtowania, której może nie potrzebować
Odpowiadająca reguła dla wywołujących to sprawdzanie. ActiveTextShaper zwraca nil, gdy platforma nie ma domyślnego i nic nie skonfigurowano, a wejście kształtowania zgłasza to jako niedostępny kształtownik, a nie jako awarię kształtowania. To różne problemy i zasługują na różne komunikaty: jeden to luka wdrożeniowa, drugi to problem fontu albo tekstu
Zainstaluj raz, zanim cokolwiek ukształtuje
Instalacja zastępuje i zwalnia poprzedni kształtownik, więc wywoływanie jej wielokrotnie jest bezpieczne, ale bezcelowe, a wywoływanie jej, gdy inny wątek kształtuje, nie jest bezpieczne wcale. Rób to podczas startu. Jeśli musisz później wrócić do domyślnego platformowego, przekaż nil, co jest też sposobem wycofania testowego dubla na końcu testu
Gdy backend jest zainstalowany, pomiar i zawijanie zachowują się tak samo na obu platformach, bo konsumują metryki przebiegów i glifów, zamiast wywoływać platformę bezpośrednio; model zawijania jest opisany w artykule o pomiarze tekstu i zawijaniu słów. Wspierane platformy i toolchainy komponentu są wypisane na stronie produktu PDFium Delphi component