PDFlibPas rozwiązuje problem znaków, których wybrana czcionka nie potrafi narysować, przeszukując łańcuch zainstalowanych czcionek zastępczych klaster po klastrze, zachowując przy tym kształtowanie i kolejność przebiegów dwukierunkowych. Włączasz go wywołaniem SetAutomaticFontFallback, rozszerzasz łańcuch przez AddFontFallback, a w pliku osadzane są tylko te czcionki zastępcze, które faktycznie posłużyły do wygenerowania wyjścia
Problem, który to rozwiązuje, spotyka każdy generator dokumentów w chwili, gdy nazwisko klienta pojawia się w piśmie, którego szablonowa czcionka nigdy nie przewidziała. Awaria jest cicha, co czyni ją kosztowną
Dlaczego nieobsługiwany tekst znika, zamiast zgłosić błąd?
Ponieważ PDF nie zna pojęcia czcionki, która nie potrafi narysować znaku. Czcionka prosta mapuje kody bajtowe na nazwy glifów poprzez kodowanie; czcionka złożona mapuje kody przez CMap na indeksy glifów. Poproś o glif, którego dana czcionka nie zawiera, a otrzymasz indeks glifu zero, .notdef, który większość czcionek rysuje jako nic albo jako pusty prostokąt. Plik jest strukturalnie poprawny, operator tekstu ma prawidłową postać, a strona się renderuje. Po prostu w miejscu, gdzie powinno być nazwisko, jest pusto
Nic w ISO 32000-1 nie wymaga, aby producent to zauważył. Generator, który zapisuje tekst bez sprawdzania pokrycia, tworzy technicznie zgodny plik PDF, który po cichu utracił treść, a strata ujawnia się na ekranie klienta dopiero po kilku tygodniach. Dlatego funkcja fallbacku i raport o brakujących glifach działają razem: rozwiązanie tego, co da się rozwiązać, to tylko połowa zadania, a zgłoszenie tego, czego rozwiązać się nie dało, to druga połowa
Fallback działa na poziomie klastra, nie punktu kodowego
Granularność to szczegół, który odróżnia działającą implementację od tylko wiarygodnie wyglądającej. Tekst nie jest sekwencją niezależnych znaków. Sylaba dewanagari, emoji z modyfikatorem odcienia skóry, litera bazowa ze znakami diakrytycznymi łączącymi się z nią: każdy z nich to jeden klaster, który musi zostać wyrenderowany przez jedną czcionkę, ponieważ decyzje kształtowania wewnątrz niego zależą od tabel w tej właśnie czcionce
PDFlibPas rozwiązuje klastry, więc klaster pokryty przez czcionkę zastępczą jest rysowany w całości przez tę czcionkę. Podzielenie klastra w połowie i narysowanie części znaków czcionką podstawową, a części zastępczą dałoby wynik technicznie obecny, lecz wizualnie zepsuty, co można argumentować jest gorsze niż pusta przestrzeń, od której zaczynałeś. Kolejność przebiegu jest również zachowana, więc fallback wewnątrz przebiegu od prawej do lewej nie zmienia kolejności otaczającego tekstu; ten sam mechanizm leży u podstaw układu pionowego opisanego w pisowni pionowej dla języka japońskiego i chińskiego
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// Kolejność przeszukiwania: wygrywa pierwsze dopasowanie, więc najszersze czcionki umieść na końcu
Lib.AddFontFallback('Microsoft YaHei'); // uproszczony chiński
Lib.AddFontFallback('Meiryo'); // japoński
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = osadź czcionkę
Lib.SetTextSize(11);
Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
Lib.DrawText(72, 700, 'Delivery status: on time');
Lib.SaveToFile('invoice.pdf');
finally
Lib.Free;
end;
end;
Ustaw kolejność łańcucha świadomie. Rozwiązywanie wybiera pierwszą czcionkę, która pokrywa dany klaster, więc szeroka czcionka pan-unikodowa umieszczona na początku przechwyci niemal wszystko, a starannie dobrane czcionki dla konkretnych pism nigdy nie zostaną wykorzystane. Umieść czcionki specyficzne na początku, a uniwersalną na końcu
Zgłoszenie czy przerwanie: jaką awarię wolisz?
SetMissingGlyphPolicy przyjmuje PDF_MISSING_GLYPH_REPORT, zgodną wartość domyślną, albo PDF_MISSING_GLYPH_ABORT. Przy zasadzie raportowania operacja tekstowa przebiega dalej, nierozwiązywalne punkty kodowe są odrzucane jak dotychczas, a każdy z nich zostaje zapisany. Przy zasadzie przerywania operacja tekstowa zostaje odrzucona, zanim jakakolwiek treść zostanie zapisana, a LastErrorCode przyjmuje wartość 521
Wybieraj w zależności od przeznaczenia dokumentu. Partia raportów wewnętrznych powinna kontynuować renderowanie i logować luki, bo nieco niekompletny raport dziś jest lepszy niż brak raportu w ogóle. Prawnie wiążąca umowa, faktura albo cokolwiek z czyimś nazwiskiem powinno przerywać działanie, ponieważ po cichu pominięty znak w nazwie strony to defekt, który wolisz odkryć we własnym procesie niż w sporze. Zasada przerywania kończy się niepowodzeniem przed zapisem, więc żaden na wpół uformowany strumień treści nie zostaje po sobie
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... zbuduj dokument ...
if Lib.DrawText(72, 660, CustomerName) <> 1 then
if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
begin
Report := Lib.GetMissingGlyphReportJSON;
// {"valid":false,"policy":1,"eventCount":1,"events":[
// {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
// "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
// "fontType":"TrueType","operation":"DrawText"}]}
EscalateToOperator(Report);
end;
finally
Lib.Free;
end;
end;
Raport jest celowo przygotowany do odczytu maszynowego i ograniczony. Każde zdarzenie zawiera stronę, indeks UTF-16 wewnątrz łańcucha znaków, punkt kodowy zarówno w postaci liczbowej, jak i w formie U+XXXX, wybraną czcionkę, jej typ oraz operację, w której wystąpił problem, dzięki czemu zgłoszenie wsparcia może wskazać dokładny znak zamiast opisywać objaw. Tracker przechowuje 256 najnowszych zdarzeń, co wystarcza do zdiagnozowania dokumentu i jest na tyle mało, że patologiczny przebieg nie zamieni diagnostyki w problem z pamięcią
Pomiar i rysowanie muszą być zgodne
Pomiar szerokości wykorzystuje te same decyzje fallbacku uwzględniające klastry co rysowanie. Brzmi to oczywiście, a jest właśnie tym, co najczęściej psuje się w domowej roboty warstwach fallbacku: łatają ścieżkę rysowania, zostawiają pomiar na czcionce podstawowej, a każde pole tekstowe, wyrównanie do prawej i kolumna tabeli kończy wyliczenia na szerokościach niezgodnych z tym, co faktycznie zostało wyrenderowane
Ponieważ obie ścieżki współdzielą to samo rozwiązywanie, łańcuch znaków zmierzony przed narysowaniem zajmuje dokładnie taką szerokość, na jaką został zmierzony, wliczając przebiegi fallbacku. To właśnie sprawia, że fallback można bezpiecznie włączyć globalnie, a nie tylko w miejscach sprawdzonych ręcznie
Osadzane jest tylko to, czego faktycznie użyto
Czcionki zastępcze są osadzane leniwie: czcionka w łańcuchu, która nigdy nie rozwiązała żadnego klastra, nie wnosi nic do wyjścia. Dokument zawierający jeden chiński znak i 5000 znaków łacińskich nie niesie pełnej czcionki CJK; niesie to, co wygenerował proces tworzenia podzbioru dla tego jednego glifu, co jest zachowaniem opisanym w optymalizacji rozmiaru pliku i tworzeniu podzbiorów czcionek
Ta leniwość sprawia, że skonfigurowanie szerokiego łańcucha jest tanie. Zarejestruj czcionki, których może potrzebować twój zestaw dokumentów w każdej obsługiwanej lokalizacji, a każdy pojedynczy plik PDF zapłaci tylko za to, czego faktycznie użył. Dla dokumentów, których nie wygenerowałeś, gdzie brakujące czcionki są już wewnątrz istniejącego pliku, ścieżka naprawy jest inna i została opisana w osadzaniu brakujących czcionek w istniejącym pliku PDF
Warto jasno powiedzieć o jednym zastrzeżeniu wdrożeniowym: fallback rozwiązuje się względem czcionek zainstalowanych na maszynie, na której działa kod. Serwer bez zainstalowanych czcionek CJK nie ma do czego się odwołać, a raport powie ci to już przy pierwszym dokumencie, a nie dopiero po pierwszej skardze. Dostarczaj czcionki, od których zależysz, i potwierdź licencję na ich osadzanie
PDFlibPas to biblioteka PDF dla Delphi, C++Builder i Lazarus z odpowiadającymi jej interfejsami DLL i ActiveX, dzięki czemu API fallbacku i brakujących glifów jest dostępne również z wywołań spoza Pascala. Pełna dokumentacja znajduje się na stronie biblioteki PDFlibPas dla Delphi