Technický článek

Řízení náhrady fontů PDF v Delphi pomocí PDFium

PDFium Component umožňuje aplikaci v Delphi rozhodnout, jaké bajty fontu se použijí, když PDF odkazuje na font, který nevkládá. ConfigureSystemFontProvider instaluje implementaci IPdfSystemFontProvider, která přijímá každý požadavek na mapování fontu, jaký PDFium vytvoří, kompletní s názvem řezu, řezem tučnosti, příznakem kurzívy, znakovou sadou a rodinou roztečí, a odpovídá bajty TrueType, TrueType Collection nebo OpenType, které se mají použít

Toto existuje proto, že nevložené fonty jsou loterie ve vykreslování. PDF, které pojmenuje Arial a nic nevloží, se na pracovní stanici vykreslí s Arialem, na serveru s Linuxem s metricky kompatibilní náhradou a na uzavřeném kontejnerovém obrazu s tím, co najde systémový mapovač. Stejná faktura vypadá na každém z nich jinak, řádky se posunou a zákazník obdrží dokument, který neodpovídá archivované kopii

Proč prostě neinstalovat fonty na server?

Někdy je to správná odpověď, a v takovém případě ji zvolte. Ale selhává ve třech běžných situacích. Licence mohou zakazovat instalaci fontu na server pro automatizované vykreslování. Obrazy kontejnerů se přestavují často a ručně instalovaný font zmizí s dalším nasazením. A regulované pracovní postupy vyžadují, aby byl vykreslovací zásobník reprodukovatelný z artefaktů pod správou verzí, což celosystémová instalace fontu není

Poskytovatel řeší všechny tři situace přenesením rozhodnutí do vaší aplikace. Fonty se dodávají jako prostředky, které kontrolujete, mapovací politika je kód, který můžete revidovat, a stejný binární soubor se vykresluje identicky všude, protože nic nezávisí na tom, co je náhodou nainstalováno

Instalace poskytovatele

Konfigurace musí proběhnout před načtením knihovny. PDFium přebírá strukturu informací o systémovém fontu při inicializaci a poté drží handly, které vydává, takže výměna poskytovatele, zatímco jsou dokumenty otevřené, by znehodnotila handly fontů, které PDFium stále drží; komponenta to rovnou odmítne, místo aby nechala poškodit vykreslování:

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
  // Deterministické mapování: o tom, který soubor pro tento požadavek
  // dodáme, rozhoduje název řezu plus tučnost a kurzíva
  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);   // kompletní bajty sfnt nebo TTC
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // index uvnitř kolekce
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // vše rozhoduje hostitel
  Policy.AllowFaceSubstitution := False;  // odmítnout jiný název řezu
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Teprve nyní načtěte knihovnu a otevírejte dokumenty
end;

Ukončení probíhá v opačném pořadí: poskytovatel se nejprve odpojí od PDFium, poté se knihovna uvolní. Vynechání odpojení ponechá nativní handly fontů ukazující na objekty Pascalu, které se za chvíli uvolní, což je klasická přístupová chyba při ukončení v kódu, jenž míchá referenčně počítaná rozhraní s knihovnou v C

Co příznaky politiky ve skutečnosti rozhodují

AllowDefaultFallback je přepínač mezi dvěma provozními režimy. Když je vypnutý, požadavek, který poskytovatel odmítne, prostě selže, což chcete, když dokazujete, že je v korpusu zohledněn každý font: jakákoli mezera se okamžitě zviditelní, místo aby byla zamaskována. Když je zapnutý, nevyřešené požadavky se delegují na mapovač vrácený FPDF_GetDefaultSystemFontInfo, přičemž vnější svět stále vidí jeden jednotný obal handle, s názvem řezu, znakovou sadou, daty tabulky a mazáním fontu správně směrovaným podle původu

AllowFaceSubstitution řídí, zda poskytovatel smí odpovědět jiným názvem řezu, než jaký byl požadován. Jeho vypnutí dělá z náhrady výslovné rozhodnutí místo náhody, což má význam, když dokument pojmenuje font, jehož metriky se liší dost na to, aby to změnilo stránkování

Komponenta validuje každou odpověď poskytovatele, než se dostane k PDFium: prázdná data jsou odmítnuta, předimenzované fonty jsou odmítnuty proti MaxFontBytes, index TTC se kontroluje a jednotlivé tabulky sfnt se obsluhují z adresáře fontu, když PDFium žádá o tabulku místo o celý soubor. Tato poslední schopnost znamená, že poskytovatel může předat kompletní soubor fontu a nechat komponentu odpovídat na dotazy na úrovni tabulky, místo aby vystavoval syrové objekty Pascalu přes C ABI

Ukládání do mezipaměti bez visících dat fontu

Požadavky na mapování fontu se během vykreslování opakují neustále, takže odpovědi se ukládají do mezipaměti s klíčem pokrývajícím každý parametr volby fontu, vyřazovaným v omezeném pořadí nejméně nedávno použitých. Jemnost je v životnosti: PDFium může stále číst bajty fontu, jehož položka mezipaměti byla právě vyřazena

Mezipaměť ukládá referenčně počítaná dynamická pole a každý nativní handle drží vlastní snímek, takže vyřazení uvolní referenci, ne paměť, která se právě používá. Zpětné volání pro smazání uvolní handle a udržuje počet aktivních položek. Prakticky to znamená, že MaxCacheEntries lze ladit kvůli paměti bez jakéhokoli rizika vytažení dat zpod probíhajícího vykreslování

Je poskytovatel volán v mém vlákně?

Ne, nikoli nutně. PDFium může volat mapovač z vlastních pracovních vláken, takže implementace musí být bezpečná pro vlákna. Sdílené čítače, mezipaměť a sledování konfigurace jsou uvnitř komponenty každé chráněny vlastní kritickou sekcí, ale kód uvnitř ResolveFont musíte zabezpečit vy

Nejbezpečnější podoba je poskytovatel, který se nedotýká žádného sdíleného měnitelného stavu: čte z tabulky sestavené při startu, načte bajty ze souboru nebo prostředku, vrátí. Pokud vyhledávání potřebuje vlastní sdílenou mezipaměť, zajistěte ji. A udržujte výjimky uvnitř své implementace, protože výjimka Pascalu nikdy nesmí projít zásobníkem PDFium; komponenta zachytává na hranici C ABI a převádí na selhání nebo volitelnou výchozí náhradu, ale spoléhat se na to jako na běžný tok řízení stojí výkon a skrývá chyby. Pravidla vláken pro zbytek komponenty se řídí stejnými principy jako v článku disciplína zámku vykreslování

Doložení mapování v produkčním provozu

Statistiky mění náhradu fontů z hádání na něco, co lze ověřit assertions. GetSystemFontProviderStatistics hlásí, zda je poskytovatel nakonfigurován a nainstalován, kolik požadavků na mapování bylo vytvořeno a jak byly uspokojeny, rozdělené na zásahy mezipaměti, zásahy poskytovatele a zásahy výchozí náhrady, spolu s odmítnutými odpověďmi, selhanými požadavky, živými handly a fonty v mezipaměti:

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

  // Při běhu ověřování shody s vypnutou náhradou znamená jakýkoli
  // zásah náhrady nebo selhaný požadavek, že dokument odkázal na
  // font, který nedodáváme
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Rostoucí počet RejectedProviderResponses je signál, že poskytovatel odpovídá daty, která politika odmítá, obvykle předimenzovaným souborem nebo nahrazeným řezem, a stojí za to na to upozorňovat, protože takové požadavky se tiše degradují na náhradu nebo selhání. Pro diagnostiku toho, jaké fonty dokument skutečně potřebuje ještě před sestavením mapovací tabulky, uvádí cesta inspekce v článku analýza vlastností fontů PDF vložené i nevložené fonty podle dokumentu

Zajišťování fontů, vykreslování a extrakce textu sdílejí stejnou instanci knihovny napříč Delphi, C++Builder a Lazarus; podrobnosti nasazení popisuje stránka PDFium Component pro Delphi