PDFium Component pozwala aplikacji Delphi decydować, jakich bajtów fontu użyć, gdy PDF odwołuje się do fontu, którego nie osadza. ConfigureSystemFontProvider instaluje implementację IPdfSystemFontProvider, która odbiera każde żądanie mapowania fontu wystawiane przez PDFium, wraz z nazwą kroju, grubością, flagą kursywy, zestawem znaków i rodziną szerokości, i odpowiada bajtami TrueType, TrueType Collection albo OpenType do użycia
Istnieje to dlatego, że nieosadzone fonty to loteria renderowania. PDF, który nazywa Arial i niczego nie osadza, renderuje się z Arialem na stacji roboczej, z zamiennikiem zgodnym metrycznie na serwerze Linux i z czymkolwiek, co znajdzie mapper hosta na zamkniętym obrazie kontenera. Ta sama faktura wygląda na każdym z nich inaczej, złamania wierszy się przesuwają, a klient otrzymuje dokument niezgodny z zarchiwizowaną kopią
Dlaczego po prostu nie zainstalować fontów na serwerze?
Czasem to jest odpowiedź i wtedy warto z niej skorzystać. Ale zawodzi w trzech powszechnych sytuacjach. Licencja może zabraniać instalowania fontu na serwerze do automatycznego renderowania. Obrazy kontenerów są przebudowywane często, a font zainstalowany ręcznie znika przy kolejnym wdrożeniu. A uregulowane przepływy pracy wymagają, by stos renderowania był odtwarzalny z artefaktów pod kontrolą wersji, czym instalacja fontu na poziomie całej maszyny nie jest
Dostawca rozwiązuje wszystkie trzy problemy, przenosząc decyzję do twojej aplikacji. Fonty są dostarczane jako zasoby, które kontrolujesz, zasada mapowania to kod, który możesz zrecenzować, a ten sam plik binarny renderuje identycznie wszędzie, ponieważ nic nie zależy od tego, co akurat jest zainstalowane
Instalowanie dostawcy
Konfiguracja musi nastąpić przed wczytaniem biblioteki. PDFium przyjmuje strukturę informacji o foncie systemowym przy inicjalizacji i zachowuje uchwyty, które wydaje potem, więc podmiana dostawcy przy otwartych dokumentach unieważniłaby uchwyty fontów, które PDFium wciąż trzyma; komponent od razu to odrzuca zamiast pozwolić, by uszkodziło renderowanie:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// Deterministyczne mapowanie: nazwa kroju plus grubość i kursywa decydują,
// który plik dostarczamy dla tego żądania
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // kompletne bajty sfnt albo TTC
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // indeks wewnątrz kolekcji
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // host decyduje o wszystkim
Policy.AllowFaceSubstitution := False; // odrzuć inną nazwę kroju
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// Dopiero teraz wczytaj bibliotekę i otwórz dokumenty
end;
Demontaż przebiega w odwrotnej kolejności: dostawca jest najpierw odłączany od PDFium, a dopiero potem biblioteka jest wyładowywana. Pominięcie odłączenia pozostawia natywne uchwyty fontów wskazujące na obiekty Pascala, które zaraz zostaną zwolnione, co jest klasycznym naruszeniem dostępu przy zamykaniu w kodzie mieszającym interfejsy ze zliczaniem referencji z biblioteką C
Co dokładnie decydują flagi zasady
AllowDefaultFallback to przełącznik między dwoma trybami działania. Gdy jest wyłączony, żądanie odrzucone przez dostawcę po prostu zawodzi, co jest pożądane, gdy dowodzisz, że każdy font w korpusie jest uwzględniony: każda luka staje się widoczna od razu zamiast być zamaskowana. Gdy jest włączony, nierozwiązane żądania są delegowane do mappera zwracanego przez FPDF_GetDefaultSystemFontInfo, podczas gdy świat zewnętrzny wciąż widzi jeden jednolity opakowany uchwyt, z nazwą kroju, zestawem znaków, danymi tabeli i usuwaniem fontu poprawnie kierowanym według pochodzenia
AllowFaceSubstitution rządzi tym, czy dostawca może odpowiedzieć inną nazwą kroju niż żądana. Wyłączenie tego czyni podstawienie jawną decyzją zamiast przypadku, co ma znaczenie, gdy dokument nazywa font, którego metryki różnią się na tyle, by zmienić paginację
Komponent waliduje każdą odpowiedź dostawcy, zanim dotrze do PDFium: puste dane są odrzucane, zbyt duże fonty są odrzucane wobec MaxFontBytes, indeks TTC jest sprawdzany, a poszczególne tabele sfnt są serwowane z katalogu fontu, gdy PDFium pyta o tabelę zamiast o cały plik. Ta ostatnia możliwość oznacza, że dostawca może przekazać kompletny plik fontu i pozwolić komponentowi odpowiadać na zapytania na poziomie tabel, zamiast wystawiać surowe obiekty Pascala przez granicę ABI C
Buforowanie bez wiszących danych fontu
Żądania mapowania fontów powtarzają się nieustannie podczas renderowania, więc odpowiedzi są buforowane pod kluczem obejmującym każdy parametr wyboru fontu, usuwane w ograniczonej kolejności najdawniej używanych. Subtelnością jest cykl życia: PDFium może wciąż odczytywać bajty fontu, którego wpis w buforze właśnie usunięto
Bufor przechowuje tablice dynamiczne ze zliczaniem referencji, a każdy natywny uchwyt trzyma własny migawkowy obraz, więc usunięcie z bufora zmniejsza licznik referencji zamiast zwalniać pamięć w użyciu. Callback usuwania zwalnia uchwyt i utrzymuje licznik aktywnych. W praktyce oznacza to, że MaxCacheEntries można dostroić pod kątem pamięci bez ryzyka wyrwania danych spod trwającego renderowania
Czy dostawca jest wywoływany w moim wątku?
Nie, niekoniecznie. PDFium może wywoływać mapper z własnych wątków roboczych, więc implementacja musi być bezpieczna wątkowo. Współdzielone liczniki, bufor i obserwacja konfiguracji są każde chronione wewnątrz komponentu własną sekcją krytyczną, ale to na tobie spoczywa uczynienie bezpiecznym kodu wewnątrz ResolveFont
Najbezpieczniejszy kształt to dostawca, który nie dotyka żadnego zmiennego stanu współdzielonego: czytaj z tabeli zbudowanej przy starcie, wczytaj bajty z pliku albo zasobu, zwróć wynik. Jeśli wyszukiwanie potrzebuje własnego współdzielonego bufora, zabezpiecz go. I trzymaj wyjątki wewnątrz swojej implementacji, ponieważ wyjątek Pascala nigdy nie może rozwijać się przez stos PDFium; komponent przechwytuje na granicy ABI C i zamienia na awarię albo opcjonalny domyślny fallback, ale poleganie na tym jako normalnym przepływie sterowania kosztuje wydajność i ukrywa błędy. Reguły wątkowości dla reszty komponentu podążają za tymi samymi zasadami co te w dyscyplinie blokady renderowania
Dowodzenie poprawności mapowania na produkcji
Statystyki zamieniają podstawianie fontów z zgadywania w coś, o czym można stanowić asercje. GetSystemFontProviderStatistics zgłasza, czy dostawca jest skonfigurowany i zainstalowany, ile żądań mapowania zostało wykonanych i jak zostały zaspokojone, w podziale na trafienia w buforze, trafienia u dostawcy i trafienia domyślnego fallbacku, wraz z odrzuconymi odpowiedziami, nieudanymi żądaniami, żywymi uchwytami i zbuforowanymi fontami:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// W przebiegu weryfikacji zgodności z wyłączonym fallbackiem każde trafienie fallbacku lub
// nieudane żądanie oznacza, że dokument odwołał się do fontu, którego nie dostarczamy
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
Rosnąca liczba RejectedProviderResponses to sygnał, że dostawca odpowiada danymi, których zasada odrzuca, zwykle zbyt dużym plikiem albo podstawionym krojem, i warto na to alarmować, ponieważ takie żądania po cichu degradują się do fallbacku albo awarii. Do diagnozowania, jakich fontów dokument faktycznie potrzebuje, zanim zbudujesz tabelę mapowania, ścieżka inspekcji w analizowaniu właściwości fontów PDF wymienia fonty osadzone i nieosadzone dla każdego dokumentu
Zaopatrywanie w fonty, renderowanie i ekstrakcja tekstu współdzielą tę samą instancję biblioteki w Delphi, C++Builder i Lazarus; szczegóły wdrożenia są opisane na stronie PDFium Component dla Delphi