Техническа статия

Управление на заместването на PDF шрифтове в Delphi с PDFium

PDFium Component позволява на Delphi приложение да решава кои байтове на шрифт се използват, когато PDF файл препраща към шрифт, който не вгражда. ConfigureSystemFontProvider инсталира реализация на IPdfSystemFontProvider, която получава всяка заявка за съпоставяне на шрифт, направена от PDFium, пълна с име на лице, тегло, флаг за курсив, набор от знаци и семейство стъпка, и отговаря с байтовете на TrueType, TrueType Collection или OpenType, които да се използват

Това съществува, защото невградените шрифтове са лотария при рендиране. PDF, който назовава Arial и не вгражда нищо, се рендира с Arial на работна станция, с метрично съвместим заместител на Linux сървър, и с каквото хостовият съпоставящ механизъм намери на заключен контейнерен образ. Една и съща фактура изглежда различно на всеки, прекъсванията на редовете се местят, а клиент получава документ, който не съвпада с архивираното копие

Защо просто да не се инсталират шрифтовете на сървъра?

Понякога това е отговорът, и когато е, изберете го. Но се проваля в три чести ситуации. Лицензирането може да забранява инсталирането на шрифт на сървър за автоматизирано рендиране. Контейнерните образи се преизграждат често, а шрифт, инсталиран ръчно, изчезва при следващото разгръщане. А регулирани работни процеси се нуждаят стекът за рендиране да е възпроизводим от артефакти под контрол на версии, каквото инсталация на шрифт в целия компютър не е

Доставчик решава и трите, като премества решението във вашето приложение. Шрифтовете се доставят като ресурси, които вие контролирате, политиката на съпоставяне е код, който можете да прегледате, а един и същ бинарен файл рендира еднакво навсякъде, защото нищо не зависи от това какво се случва да е инсталирано

Инсталиране на доставчик

Конфигурирането трябва да се случи преди зареждането на библиотеката. PDFium приема структура за информация за системен шрифт при инициализация и пази манипулатори, които раздава впоследствие, така че смяната на доставчик, докато документи са отворени, би обезсилила манипулатори на шрифтове, които PDFium все още държи; компонентът отхвърля това направо, вместо да позволи да повреди рендиране:

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
  // Детерминирано съпоставяне: името на лицето плюс теглото и курсивът
  // решават кой файл доставяме за тази заявка
  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);   // пълни sfnt или TTC байтове
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // индекс вътре в колекция
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // хостът решава всичко
  Policy.AllowFaceSubstitution := False;  // отхвърли различно име на лице
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Едва сега заредете библиотеката и отворете документи
end;

Разрушаването се изпълнява в обратен ред: доставчикът първо се отделя от PDFium, след което библиотеката се разтоварва. Пропускането на отделянето оставя нативни манипулатори на шрифтове, сочещи към Pascal обекти, които всеки момент ще бъдат освободени, което е класическото нарушение на достъпа при спиране в код, който смесва интерфейси с броене на референции с C библиотека

Какво всъщност решават флаговете на политиката

AllowDefaultFallback е превключвателят между два режима на работа. Изключен, заявка, която доставчикът отказва, просто се проваля, което е желаното, докато доказвате, че всеки шрифт в корпус е отчетен: всяка празнина става видима незабавно, вместо да бъде замазана. Включен, неразрешените заявки се делегират на съпоставящия механизъм, върнат от FPDF_GetDefaultSystemFontInfo, докато външният свят все още вижда една еднородна обвивка на манипулатор, с име на лице, набор от знаци, данни на таблица и рутина за изтриване на шрифт, насочени правилно по произход

AllowFaceSubstitution управлява дали доставчик може да отговори с различно име на лице от заявеното. Изключването му прави заместването изрично решение, вместо инцидент, което има значение, когато документ назовава шрифт, чиито метрики се различават достатъчно, за да промени разбивката на страници

Компонентът валидира всеки отговор на доставчик, преди да достигне до PDFium: празни данни се отхвърлят, прекалено големи шрифтове се отхвърлят спрямо MaxFontBytes, TTC индексът се проверява, а отделни sfnt таблици се обслужват от директорията на шрифта, когато PDFium поиска таблица, а не целия файл. Тази последна способност означава, че доставчик може да предаде пълен шрифтов файл и да остави компонента да отговаря на заявки на ниво таблица, вместо да излага сурови Pascal обекти през C ABI

Кеширане без висящи данни за шрифт

Заявките за съпоставяне на шрифт се повтарят постоянно по време на рендиране, така че отговорите се кешират с ключ, покриващ всеки параметър за избор на шрифт, изваждан по ограничен ред „най-отдавна използван“ (LRU). Финият момент е жизненият цикъл: PDFium все още може да чете байтовете на шрифт, чийто запис в кеша току-що е изваден

Кешът съхранява динамични масиви с броене на референции, а всеки нативен манипулатор държи собствена снимка, така че изваждането отпада референция, вместо да освобождава памет в употреба. Обратното извикване за изтриване освобождава манипулатора и поддържа активен брояч. На практика това означава, че MaxCacheEntries може да се настройва за памет без никакъв риск от изтегляне на данни изпод рендиране в процес

Извиква ли се доставчикът в моята нишка?

Не, не непременно. PDFium може да извика съпоставящия механизъм от собствените си работни нишки, така че реализацията трябва да е безопасна за нишки. Споделени броячи, кешът и наблюдението на конфигурацията всеки са защитени вътре в компонента от собствена критична секция, но кодът вътре в ResolveFont е ваша задача да направите безопасен

Най-безопасната форма е доставчик, който не докосва променливо споделено състояние: чете от таблица, изградена при стартиране, зарежда байтове от файл или ресурс, връща. Ако търсене се нуждае от собствен споделен кеш, защитете го. И дръжте изключенията вътре във вашата реализация, тъй като Pascal изключение никога не бива да се разгъва през стека на PDFium; компонентът улавя на границата на C ABI и преобразува в провал или незадължителен резерв по подразбиране, но разчитането на това като нормален поток на управление струва производителност и крие грешки. Правилата за нишки за останалата част от компонента следват същите принципи като тези в дисциплина на заключването при рендиране

Доказване на съпоставянето в продукция

Статистиките превръщат заместването на шрифтове от гадаене в нещо, върху което можете да твърдите. GetSystemFontProviderStatistics отчита дали доставчик е конфигуриран и инсталиран, колко заявки за съпоставяне са направени, и как са били удовлетворени, разделени на попадения в кеша, попадения при доставчика и попадения при резерва по подразбиране, заедно с отхвърлени отговори, провалени заявки, живи манипулатори и кеширани шрифтове:

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]));

  // При проверка за съответствие с изключен резерв, всяко попадение
  // в резерва или провалена заявка означава, че документ е препратил
  // към шрифт, който не доставяме
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Нарастващ брой на RejectedProviderResponses е сигналът, че доставчик отговаря с данни, които политиката отказва, обикновено прекалено голям файл или заместено лице, и си струва да се сигнализира за това, защото тези заявки тихо деградират до резерв или провал. За диагностициране кои шрифтове документ действително се нуждае, преди да изградите таблицата за съпоставяне, маршрутът за инспекция в анализиране на свойствата на PDF шрифтове изброява вградени и невградени шрифтове за документ

Осигуряването на шрифтове, рендирането и извличането на текст споделят един и същ инстанс на библиотеката в Delphi, C++Builder и Lazarus; подробностите за разгръщане са описани на страницата на PDFium Component за Delphi