Technický článek

Konfigurace knihovny PDFium: když Brotli tiše vymění Skiu

V PDFium Component pro Delphi bývalo zapnutí BrotliEnabled nebo IsolatePerDocument v TPdfLibraryConfiguration přepnutím přibaleného buildu Skia na renderer AGG bez jakékoli chyby, protože obě volby zvednou FPDF_LIBRARY_CONFIG na verzi, kde PDFium čte m_RendererType doslova. Od v3.123.0 zůstává výchozí renderer defaultem samotného DLL a od v3.125.0 vyvolá požadavek po Skia nebo Fontations, kterýmu DLL nemůže vyhovět, chytitelnou výjimku EPdfError místo zabití procesu

Ani jeden bug se nehlásil. První produkoval stránky, které vypadaly v pořádku, akorát vykreslené jiným rasterizérem, s mírně odlišným anti-aliasingem a hranami textu než build, který jste odeslali a testovali. Druhý se ohlásil, a to hlasitě, tím, že sestřelil hostitelský proces zevnitř nativní inicializace. Oba vycházejí ze stejného místa: verzovaná C struktura, jejíž pole se počítají, jen jakmile to řekne číslo verze, a jejíž nulové hodnoty nejsou „nenastaveno“, ale skutečné volby

Jak FPDF_LIBRARY_CONFIG rozhoduje, který renderer PDFium použije?

FPDF_InitLibraryWithConfig konzultuje m_RendererType jen tehdy, když pole Version struktury je 4 a výš, a od té verze použije hodnotu přesně tak, jak je zapsaná. Pod verzí 4 PDFium pole ignoruje a vybere build default, který je Skia v buildech kompilovaných s PDF_USE_SKIA a AGG všude jinde

Každé pozdější pole následuje týž vzor. Struktura rostla o jednu schopnost po druhé a každá schopnost přišla spolu s novým číslem verze. PDFium Component staví nativní strukturu v LoadLibrary z vaší TPdfLibraryConfiguration a zvedá verzi jen tak daleko, jak vyžadují nastavené volby

Verze strukturyPole, které přidáváNastavuje
2m_pIsolate, m_v8EmbedderSlotVždy zapsáno; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform není nil
4m_RendererTypeRenderer jiný než prpDefault
5m_FontLibraryTypeFontBackend jiný než pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Past sedí v posledních dvou řádcích. Verze jsou kumulativní: struktura verze 6 je zároveň strukturou verze 4 i verze 5, takže PDFium čte m_RendererType a m_FontLibraryType, i když jste si přáli jen Brotli. Co v tu chvíli sedí v těch dvou polích, se stane rendererem a font backendem, ať jste je chtěli vybrat, nebo ne

Žebříček verzí FPDF_LIBRARY_CONFIG v PDFium Component od verze 2 do verze 7 ukazující, která volba TPdfLibraryConfiguration přidává m_RendererType, m_FontLibraryType, m_BrotliEnabled a m_IsolatePerDocument, a proč kumulativní verze dělají z vynulovaného pole rendereru vědomou volbu AGG, nikoli nenastavenou hodnotu na jakémkoli buildu
Každá volba zvedne verzi struktury a všechna dřívější pole zůstávají živá, takže nula v m_RendererType dorazí do PDFium jako explicitní žádost o AGG

Proč zapnutí Brotli přeplo renderer na AGG?

Před v3.123.0 zapisoval PDFium Component do m_RendererType pro prpDefault hodnotu FPDF_RENDERERTYPE_AGG, takže jakákoli konfigurace, která posunula strukturu na verzi 6 nebo 7, donutila AGG na buildu se Skia. Runtime pdfium.dll a pdfium.v8.dll dodávané s komponentou jsou buildy Skia, takže to zasáhlo výchozí nasazení, ne nějaké exotické

Tohle mapování vypadalo neškodně, když ho člověk napsal. Na verzi 2 nebo 3 se pole nikdy nečte, takže prpDefault doopravdy znamenalo „co DLL udělá“. V momentě, kdy do hry vstoupilo BrotliEnabled (verze 6) nebo IsolatePerDocument (verze 7), proměnil týž kód „bez preference“ v explicitní žádost o AGG. Nic neselhalo. PDFium se inicializovalo normálně, vykreslilo každou stránku a nevrátilo žádný kód chyby, protože z jeho pohledu volající chtěl AGG a dostal AGG

Pixel hash udělá výměnu viditelnou tam, kde screenshoty ne. Vyrenderování první stránky téhož vzorového dokumentu pod třemi konfiguracemi dalo:

  • Výchozí konfigurace: hash 502D77C3711B4ACF
  • BrotliEnabled = True s Renderer ponechaným na prpDefault: hash F75B5EB4728ADE87
  • Explicitní prpAgg: hash F75B5EB4728ADE87, identický s během Brotli

Oprava ve v3.123.0 je veřejná funkce PdfNativeRendererType, která rozvíjí TPdfRendererPreference na hodnotu zapsanou do m_RendererType. prpAgg a prpSkia se mapují jedna na jednu. prpDefault se teď mapuje na Skia, když načtené DLL exportuje FPDF_RenderPageSkia, a na AGG jinak. Tenhle export se kompiluje pod touže podmínkou PDF_USE_SKIA jako samotný Skia default, což z něj dělá jedinou vlastnost buildu pozorovatelnou zvenčí DLL. Po opravě produkuje konfigurace Brotli tentýž hash jako výchozí

Srovnání pixel hash v PDFium Component ukazující hash výchozího renderu Skia 502D77C3711B4ACF, konfiguraci BrotliEnabled před v3.123.0 odpovídající explicitnímu běhu prpAgg s hashem F75B5EB4728ADE87 a opravený wrapper rozvíjející prpDefault přes export FPDF_RenderPageSkia zpět na původní hash Skia
Pixel hash odhalí, co screenshoty skryjí: zapnuté Brotli bývalo vykreslilo každou stránku s AGG a opravený default se teď srovná s nedotčenou konfigurací

Font backend tenhle problém nikdy neměl. m_FontLibraryType se čte od verze 5 a jeho nulová hodnota, FPDF_FONTBACKENDTYPE_FREETYPE, je zároveň default PDFium, když se pole nečte vůbec. Zápis FreeType pro pfbpDefault proto reprodukuje nativní default přesně. Nulové hodnoty nejsou vždycky špatné, prostě nejsou nikdy automaticky správné

S v3.123.0 a novějším dělá startovací kód, který byste stejně napsali, přesně to, co říká:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Musí běžet, než cokoli načte nativní knihovnu
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // zvedne FPDF_LIBRARY_CONFIG na verzi 6
  // Renderer zůstává prpDefault: rozvine se na Skia na buildech, které exportují
  // FPDF_RenderPageSkia, a na AGG na buildech jen pro AGG
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Vezměte na vědomí, že BrotliEnabled učiní streamy PDF 2.0 /BrotliDecode dekódovatelnými jen tehdy, když bylo DLL samo postaveno s PDF_ENABLE_BROTLI. Příznak je žádost a na buildu bez podpory Brotli nemá žádný účinek. TPdfLibraryConfiguration.Hardened je totéž co Default, jen s AllowMachineTime False, což zablokuje dokumentovému JavaScriptu čtení skutečných hodin; rozumný startovní bod pro serverové zpracování nedůvěryhodných souborů

Co se stane, když si vyžádáte backend, který DLL neobsahuje?

PDFium nevrací chybu pro renderer nebo font backend, který buildu chybí: FPDF_InitLibraryWithConfig neprojde nativním CHECK, který se na Windows projeví jako breakpoint výjimka a bez strukturovaného exception handleru kolem volání ukončí proces. Hlavička to říká nahlas a varuje, že nepodporovaná hodnota „will similarly fail with an immediate crash“

Ty dva konkrétní případy jsou build jen pro AGG, který dostane FPDF_RENDERERTYPE_SKIA, a build bez Fontations, který dostane FPDF_FONTBACKENDTYPE_FONTATIONS. Přibalený runtime Skia je ve druhé skupině: kreslí se Skiou, ale fonty berou z FreeType. Žádost prpSkia spolu s pfbpFontations proti němu vyprodukovala na straně Delphi External exception 80000003. Když ji náhodou chytí debugger nebo exception handler, situace je pořád neobnovitelná:

  • PDFium zůstane napůl inicializované
  • Konfigurace napříč procesem už je zapečetěná, takže ConfigurePdfLibrary odmítne opravenou konfiguraci
  • Zkusit to znovu s jinou konfigurací ve stejném procesu už nejde

Tohle je opačné selhání k bugu Brotli. Tam pole drželo hodnotu, kterou si nikdo nevybral, a PDFium ji potichu přijalo. Tady pole drží hodnotu, kterou volající zvolil vědomě, a PDFium o ní nepřipouští žádnou diskusi. Obojí jsou problémy, které musí wrapper vyřešit před nativním voláním, protože po něm už není co chytat

Jak PDFium Component předkontroluje Skiu a Fontations

Od v3.125.0 validuje LoadLibrary konfiguraci po nabití exportů DLL a před voláním FPDF_InitLibraryWithConfig a promění nepodporovaný renderer nebo font backend v EPdfError se zprávou, která jmenuje křivé nastavení a alternativy. DLL se unloadne a konfigurace se odpečetí, takže volající si může vybrat jiné nastavení a načíst znovu

Samotné rozhodnutí bydlí v čisté funkci PdfLibraryConfigurationSupportError, která bere konfiguraci plus dva booleany popisující build a vrací prázdný řetězec, je-li kombinace bezpečná. Protože se nedotýká žádného nativního stavu, můžete ji volat z vlastních testů s jakoukoli kombinací schopností. Uvnitř LoadLibrary přicházejí ty dva booleany z různých druhů důkazů a zaslouží si různou míru důvěry:

  • Skia se zjišťuje z přítomnosti exportu FPDF_RenderPageSkia, téhož signálu, který používá PdfNativeRendererType. Export i renderer Skia se kompilují pod jedinou podmínkou, takže kontrola je exaktní
  • Fontations vlastní export nemá. Jedinou stopu nechává v Rust font crates, které vtáhne do binárky, takže PDFium Component prohledá načtený soubor knihovny po názvech crate skrifa a read-fonts (také read_fonts). Prohledávka běží jen tehdy, je-li vyžádáno pfbpFontations, a soubor, který se nedá číst, se počítá jako „žádné Fontations“

Kontrola Fontations je heuristika a může se splést v jednom směru: build Fontations zbavený všech těch řetězců by se odmítl, i když by mohl fungovat. Tenhle kompromis se udělal záměrně. Falešné zamítnutí stojí výjimku, kterou chytíte, a zálohu na FreeType. Falešné přijetí stojí proces

Odpečetění znamená stejně moc jako kontrola. LoadLibrary pečetí konfiguraci na samém začátku načítání, takže bez resetu by odmítnutí schopnosti nechalo ConfigurePdfLibrary odpovídat na každý pokus EPdfError „PDFium library configuration is already sealed“. Cesta odmítnutí volá nejdřív UnloadLibrary; její volání FPDF_DestroyLibrary je v tu chvíli bezpečné, protože PDFium ještě nebylo inicializováno a vrací se hned. Ostatní selhání načítání, jako chybějící DLL nebo neshoda architektury, pečeť drží, takže retry smyčka si je musí rozlišit:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // kvalifikované unitou: Windows.LoadLibrary má týž název
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Odmítnutí schopnosti unloadne DLL a odpečetí konfiguraci.
      // DLL, které se nepodařilo načíst vůbec, zůstává zapečetěné: retry nepomůže
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Všimněte si explicitního PDFium.LoadLibrary. V unitě, která používá i Windows nebo Winapi.Windows, se nekvalifikované LoadLibrary rozvine na tu unitu, která v uses klauzuli stojí poslední; je-li to Win32 funkce, volání bez parametrů se nezkompiluje s chybou počtu argumentů, která o PDFium neřekne nic

Průběh předkontroly LoadLibrary v PDFium Component, kde ConfigurePdfLibrary zapečetí konfiguraci, kontrola schopností testuje export FPDF_RenderPageSkia a důkazy řetězců skrifa, nepodporovaná žádost vyhodí chytitelné EPdfError a odpečetí pro pokus znovu, zatímco DLL, které se nikdy nenačte, nechá PdfLibraryConfigurationSealed true
Validace běží po nabití exportů a před inicializací, takže chybějící backend selže jako EPdfError, které chytíte, místo nativního CHECK, který zabije proces

Validace, která se stane ještě dřív

ConfigurePdfLibrary odmítá některé kombinace, ještě než se vůbec objeví DLL, všechny s EPdfError. Explicitní FontBackend, včetně pfbpFreeType, vyžaduje Renderer = prpSkia, protože PDFium konzultuje font backend jen u rendereru Skia. IsolatePerDocument vyžaduje nil v V8Isolate, protože PDFium si vytváří vlastní isolát na dokument a padne do nativního CHECK, podáte-li mu i svůj. Prázdné řetězce v UserFontPaths se odmítají. A jakékoli volání po prvním pokusu o načtení selže s „PDFium library configuration is already sealed“

Poslední pravidlo má praktický důsledek: nemůžete nejdřív vyslechnout DLL a konfigurovat ho potom. GetSkiaRenderCapabilities, V8FeaturesAvailable, otevření dokumentu a většina ostatních vstupních bodů volá LoadLibrary interně, což zapečetí konfiguraci na místě. Pozdější volání UnloadLibrary ji znovu neotevře taky. Nejdřív nakonfigurujte, pak načtěte, pak se ptejte, což je přesně pořadí, jaké má diagnostická rutina dodržet:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kopie, bezpečná k prohlédnutí
  if not PDFium.Loaded then
  begin
    if PdfLibraryConfigurationSealed then
      Exit('PDFium failed to load; configuration is sealed');
    Exit('PDFium not loaded; configuration can still change');
  end;
  // Táž rezoluce, kterou aplikoval LoadLibrary, když stavěl FPDF_LIBRARY_CONFIG
  if PdfNativeRendererType(Config.Renderer,
    GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
    Renderer := 'Skia'
  else
    Renderer := 'AGG';
  Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
    [Renderer, BoolToStr(Config.BrotliEnabled, True),
     BoolToStr(Config.IsolatePerDocument, True)]);
end;

Zalogovat tenhle řádek jednou při startu je levné a je to první, co budete chtít v support ticketu, který tvrdí „text na serveru vypadá jinak“. PDFium.Loaded je kvalifikované ze stejného důvodu jako LoadLibrary: uvnitř metody formuláře nebo komponenty se holé Loaded naváže na TComponent.Loaded

Dva způsoby, jak se pokazí verzovaná C konfigurační struktura

Každá verzovaná konfigurační struktura, ať už je to FPDF_LIBRARY_CONFIG, Win32 záznam s cbSize, nebo plugin ABI, selhává dvěma souměrnými způsoby a wrapper se musí bránit proti oběma. První je naplnění pole při ponechané příliš nízké verzi; druhé je zvednutí verze při ponechaném poli na nulové hodnotě, kterou knihovna čte jako vědomou volbu

  1. Pole nastavené, verze příliš nízká. Zapište m_BrotliEnabled = 1 do struktury verze 2 a PDFium se na něj nikdy nepodívá. Volání uspěje a streamy Brotli zůstanou nedekódovatelné. Obranou je odvozovat verzi z polí, která jsou doopravdy v používání, což dělá LoadLibrary, místo jednu natvrdo zapsat
  2. Verze dost vysoká, nulové pole něco znamená. Zvedněte verzi na 6 a všechna pole až do verze 6 jsou teď živá. FillChar vynuluje m_RendererType na FPDF_RENDERERTYPE_AGG, což je skutečný renderer, ne „nenastaveno“. Obranou je zapsat každé pole, které zvolená verze pokrývá, úmyslnou hodnotou, a rozvíjet „default“ proti skutečnému buildu místo ho předpokládat

Pro hodnoty, které dokážou zabít volaného, plyne třetí pravidlo: validujte je proti tomu, co binárka dovede, dřív než volání, s nejsilnějším dostupným důkazem, a buďte upřímní v kódu i dokumentaci, když je ten důkaz heuristika. Exportovaný symbol je důkaz. Název crate v tabulce řetězců je dobrý tip

Rychlý přehled: konfigurace knihovny PDFium Component

  • Zavolejte ConfigurePdfLibrary jednou, než cokoli načte DLL; jakýkoli dotaz na schopnosti nebo načtení dokumentu konfiguraci zapečetí
  • Přejděte na v3.123.0 a novější, nastavujete-li BrotliEnabled nebo IsolatePerDocument a čekáte-li výstup Skia z přibalených runtime
  • Nechávejte Renderer na prpDefault, pokud nepotřebujete konkrétní rasterizér; teď se rozvine na build default na každé verzi struktury
  • Používejte PdfNativeRendererType s GetSkiaRenderCapabilities.PageRender, abyste zalogovali, který renderer doopravdy běží
  • Čekejte EPdfError, ne pád, pro prpSkia na DLL jen pro AGG nebo pfbpFontations na DLL bez Fontations ve v3.125.0 a novějším
  • Po odmítnutí schopnosti je PdfLibraryConfigurationSealed False a smíte překonfigurovat; po selhání načtení DLL zůstává True
  • Berte detekci Fontations jako heuristiku a držte zálohu na FreeType
  • Pište PDFium.LoadLibrary a PDFium.Loaded s názvem unity, abyste se vyhnuli kolizím názvů s Win32 a TComponent

Selhává-li DLL dřív, než má konfigurace vůbec šanci záležet, začněte s diagnostikou selhání načítání PDFium DLL v Delphi a o tom, jak komponenta najde správnou binárku na každé platformě, pojednává načítání nativní knihovny PDFium na jakémkoli cíli. Jakmile je renderer vyřízený, taktiky render cache a plynulého zoomu popisují, jak udržet vykreslování stránek rychlé v prohlížeči

PDFium Component obaluje jádro PDFium pro Delphi a C++Builder s kontrolami konfigurace, jako jsou tyhle, takže nativní inicializace selhává jako Pascal výjimka, kterou obsloužíte, místo ukončení procesu. Detaily produktu a stažení najdete na stránce produktu PDFium Component pro Delphi