Articol tehnic

Config bibliotecă PDFium: când Brotli înlocuiește tăcut Skia

În PDFium Component pentru Delphi, pornirea lui BrotliEnabled sau IsolatePerDocument în TPdfLibraryConfiguration comuta pe furiș build-ul Skia inclus pe renderer-ul AGG, fără nicio eroare, pentru că ambele opțiuni ridică FPDF_LIBRARY_CONFIG la o versiune la care PDFium citește m_RendererType literal. Din v3.123.0 renderer-ul implicit rămâne implicitul propriu al DLL-ului, iar din v3.125.0 o cerere Skia sau Fontations pe care DLL-ul nu o poate onora ridică un EPdfError prinsibil în loc să omoare procesul

Niciunul dintre bug-uri nu s-a anunțat. Primul producea pagini care arătau bine, doar randate de alt rasterizator, cu anti-aliasing și margini de text ușor diferite față de build-ul pe care l-ați livrat și l-ați testat. Al doilea s-a anunțat, zgomotos, ducând jos procesul host din interiorul inițializării native. Ambele vin din același loc: o structură C versionată ale cărei câmpuri contează doar când numărul de versiune spune asta, iar valorile zero nu înseamnă „nesetat”, ci alegeri reale

Cum decide FPDF_LIBRARY_CONFIG ce renderer folosește PDFium?

FPDF_InitLibraryWithConfig consultă m_RendererType doar când câmpul Version al structurii e 4 sau mai mare, iar de la versiunea aceea folosește valoarea exact cum e scrisă. Sub versiunea 4, PDFium ignoră câmpul și alege implicitul build-ului, care e Skia în build-urile compilate cu PDF_USE_SKIA și AGG în rest

Fiecare câmp ulterior urmează același tipar. Structura a crescut cu o capacitate pe rând, iar fiecare capacitate a sosit împreună cu un nou număr de versiune. PDFium Component construiește structura nativă în LoadLibrary din TPdfLibraryConfiguration vostru și ridică versiunea doar atât cât cer opțiunile setate

Versiune structurăCâmpul pe care îl adaugăSetat de
2m_pIsolate, m_v8EmbedderSlotScris mereu; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform nu nil
4m_RendererTypeRenderer altul decât prpDefault
5m_FontLibraryTypeFontBackend altul decât pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Capcana e în ultimele două rânduri. Versiunile sunt cumulative: o structură de versiune 6 e și o structură de versiune 4 și una de versiune 5, deci PDFium citește m_RendererType și m_FontLibraryType deși ați cerut doar Brotli. Orice stă în acele două câmpuri în clipa aceea devine renderer-ul și backend-ul de font, fie că ați vrut să le alegeți, fie că nu

Scara de versiuni FPDF_LIBRARY_CONFIG PDFium Component de la versiunea 2 la versiunea 7 arătând ce opțiune TPdfLibraryConfiguration adaugă m_RendererType, m_FontLibraryType, m_BrotliEnabled și m_IsolatePerDocument, și de ce versiunile cumulative fac un câmp de renderer zerorizat o alegere AGG deliberată, nu o valoare nesetată, pe orice build
Fiecare opțiune ridică versiunea structurii, iar fiecare câmp anterior rămâne viu, deci zero-ul din m_RendererType ajunge la PDFium drept o cerere AGG explicită

De ce pornirea Brotli comuta renderer-ul pe AGG?

Înainte de v3.123.0, PDFium Component scria FPDF_RENDERERTYPE_AGG în m_RendererType pentru prpDefault, deci orice configurație care împingea structura la versiunea 6 sau 7 forța AGG pe un build Skia. Runtime-urile pdfium.dll și pdfium.v8.dll care vin cu componenta sunt build-uri Skia, deci asta lovea deployment-ul implicit, nu unul exotic

Maparea părea inofensivă când a fost scrisă. La versiunea 2 sau 3 câmpul nu e niciodată citit, deci prpDefault chiar însemna „orice face DLL-ul”. În clipa în care BrotliEnabled (versiunea 6) sau IsolatePerDocument (versiunea 7) au intrat în imagine, același cod a transformat „fără preferință” într-o cerere AGG explicită. Nimic nu a picat. PDFium s-a inițializat normal, a randat fiecare pagină și a întors niciun cod de eroare, pentru că din perspectiva lui apelantul ceruse AGG și primise AGG

Un hash de pixeli face schimbarea vizibilă acolo unde capturile de ecran nu o fac. Randarea primei pagini a aceluiași document de probă sub trei configurații a dat:

  • Configurație implicită: hash 502D77C3711B4ACF
  • BrotliEnabled = True cu Renderer lăsat pe prpDefault: hash F75B5EB4728ADE87
  • prpAgg explicit: hash F75B5EB4728ADE87, identic cu rularea Brotli

Repararea din v3.123.0 e funcția publică PdfNativeRendererType, care rezolvă un TPdfRendererPreference la valoarea scrisă în m_RendererType. prpAgg și prpSkia se mapează unu-la-unu. prpDefault se mapează acum la Skia când DLL-ul încărcat exportă FPDF_RenderPageSkia și la AGG altfel. Export-ul acela e compilat sub aceeași condiție PDF_USE_SKIA ca și implicitul Skia însuși, ceea ce îl face singura proprietate de build pe care o poți observa din exteriorul DLL-ului. După reparare, configurația Brotli produce același hash ca cea implicită

Comparație de hash de pixeli PDFium Component arătând hash-ul de randare Skia implicit 502D77C3711B4ACF, configurația BrotliEnabled de dinainte de v3.123.0 care se potrivea cu o rulare prpAgg explicită cu hash-ul F75B5EB4728ADE87, și wrapper-ul reparat care rezolvă prpDefault prin export-ul FPDF_RenderPageSkia înapoi la hash-ul Skia original
Un hash de pixeli prinde ce ascund capturile de ecran: pornirea Brotli renda fiecare pagină cu AGG, iar implicitul reparat se potrivește acum cu configurația neatinsă

Backend-ul de font nu a avut niciodată aceeași problemă. m_FontLibraryType e citit de la versiunea 5, iar valoarea lui zero, FPDF_FONTBACKENDTYPE_FREETYPE, e și implicitul PDFium când câmpul nu e citit deloc. Scrierea FreeType pentru pfbpDefault reproduce deci exact implicitul nativ. Valorile zero nu sunt întotdeauna greșite, pur și simplu nu sunt niciodată corecte automat

Cu v3.123.0 sau mai nou, codul de pornire pe care l-ați scrie natural face acum ce spune:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Trebuie rulată înainte ca orice să încarce biblioteca nativă
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // ridică FPDF_LIBRARY_CONFIG la versiunea 6
  // Renderer rămâne prpDefault: rezolvat la Skia pe build-urile care exportă
  // FPDF_RenderPageSkia și la AGG pe build-urile doar-AGG
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Rețineți că BrotliEnabled face stream-urile /BrotliDecode PDF 2.0 decodabile doar când DLL-ul însuși a fost construit cu PDF_ENABLE_BROTLI. Flag-ul e o cerere, iar pe un build fără suport Brotli nu are niciun efect. TPdfLibraryConfiguration.Hardened e la fel ca Default, cu excepția că AllowMachineTime e False, ceea ce blochează JavaScript-ul documentului să citească ceasul real; e un punct de pornire rezonabil pentru procesarea pe server a fișierelor fără încredere

Ce se întâmplă când ceri un backend pe care DLL-ul nu îl conține?

PDFium nu întoarce o eroare pentru un renderer sau un backend de font lipsă din build: FPDF_InitLibraryWithConfig pice un CHECK nativ, care pe Windows iese la suprafață drept excepție de breakpoint și, fără un structured exception handler în jurul apelului, termină procesul. Header-ul spune la fel de clar, avertizând că o valoare nesuportată „va pica la fel cu un crash imediat”

Cele două cazuri concrete sunt un build doar-AGG care primește FPDF_RENDERERTYPE_SKIA, și un build fără Fontations care primește FPDF_FONTBACKENDTYPE_FONTATIONS. Runtime-ul Skia inclus e în a doua grupă: randează cu Skia dar folosește FreeType pentru fonturi. Cererea prpSkia împreună cu pfbpFontations contra lui producea External exception 80000003 pe partea Delphi. Când debugger-ul sau un handler de excepții se întâmplă să-l prindă, situația rămâne oricum nerecuperabilă:

  • PDFium rămâne pe jumătate inițializat
  • Configurația la nivel de proces e deja sigilată, deci ConfigurePdfLibrary refuză o configurație corectată
  • Reîncercarea cu o configurație diferită în același proces nu mai e posibilă

Asta e eșecul opus bug-ului Brotli. Acolo câmpul conținea o valoare pe care nimeni nu o alesese, iar PDFium o accepta în tăcere. Aici câmpul conține o valoare pe care apelantul a ales-o în mod deliberat, iar PDFium nu acceptă nicio discuție despre ea. Ambele sunt probleme pe care un wrapper trebuie să le rezolve înaintea apelului nativ, pentru că după el nu mai rămâne nimic de prins

Cum preverifică PDFium Component Skia și Fontations

Din v3.125.0, LoadLibrary validează configurația după legarea export-urilor DLL-ului și înainte de a apela FPDF_InitLibraryWithConfig, și transformă un renderer sau un backend de font nesuportat într-un EPdfError cu un mesaj care numește setarea vinovată și alternativele. DLL-ul este descărcat, iar configurația este desigilată, deci apelantul poate alege alte setări și se poate încărca din nou

Decizia în sine locuiește în funcția pură PdfLibraryConfigurationSupportError, care primește configurația plus doi boolean-i care descriu build-ul și întoarce un șir gol când combinația e sigură. Fiindcă nu atinge nicio stare nativă, o puteți apela din testele voastre cu orice combinație de capacități. În interiorul lui LoadLibrary cei doi boolean-i vin din feluri diferite de dovezi, și merită niveluri diferite de încredere:

  • Skia e detectat din prezența export-ului FPDF_RenderPageSkia, același semnal pe care îl folosește PdfNativeRendererType. Export-ul și renderer-ul Skia sunt compilați sub o singură condiție, deci verificarea e exactă
  • Fontations nu are export propriu. Singura urmă pe care o lasă sunt crate-urile Rust de font pe care le trage în binar, deci PDFium Component scannează fișierul bibliotecii încărcate după numele de crate skrifa și read-fonts (și read_fonts). Scanarea rulează doar când e cerut pfbpFontations, iar un fișier care nu poate fi citit contează drept „fără Fontations”

Verificarea Fontations e o euristică, și poate greși într-o direcție: un build Fontations curățat de fiecare dintre șirurile acelea ar fi respins deși ar fi putut funcționa. Compromisul acela a fost făcut intenționat. O respingere falsă vă costă o excepție pe care o puteți prinde și un fallback la FreeType. O acceptare falsă vă costă procesul

Desigilarea contează la fel de mult ca verificarea. LoadLibrary sigilează configurația chiar la începutul încărcării, deci fără resetarea aceea o respingere de capacitate ar lăsa ConfigurePdfLibrary să răspundă la orice reîncercare cu EPdfError „PDFium library configuration is already sealed”. Calea de respingere apelează întâi UnloadLibrary; apelul lui de FPDF_DestroyLibrary e sigur în clipa aceea fiindcă PDFium nu a fost încă inițializat și se întoarce imediat. Alte eșecuri de încărcare, precum un DLL lipsă sau un mismatch de arhitectură, păstrează sigiliul, deci o buclă de reîncercare trebuie să le deosebească:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // calificat cu unit-ul: Windows.LoadLibrary are același nume
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // O respingere de capacitate descarcă DLL-ul și desigilează configurația.
      // Un DLL care a piceat să se încarce de loc rămâne sigilat: reîncercarea nu ajută
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Observați PDFium.LoadLibrary explicit. Într-un unit care folosește și Windows sau Winapi.Windows, un LoadLibrary necalificat se rezolvă la ce unit apare ultima în clauza uses; când aceea e funcția Win32, apelul fără parametri nu compilează, cu o eroare de număr de argumente care nu spune nimic despre PDFium

Flux de preverificare LoadLibrary PDFium Component în care ConfigurePdfLibrary sigilează configurația, verificarea de capacitate testează export-ul FPDF_RenderPageSkia și dovezile de șiruri skrifa, o cerere nesuportată ridică un EPdfError prinsibil și desigilează pentru reîncercare, în timp ce un DLL care nu se încarcă niciodată păstrează PdfLibraryConfigurationSealed true
Validarea rulează după legarea export-urilor și înaintea inițializării, deci un backend lipsă pice drept un EPdfError pe care îl poți prinde, în locul unui CHECK nativ care omoară procesul

Validare care se întâmplă și mai devreme

ConfigurePdfLibrary respinge unele combinații înainte ca vreun DLL să fie implicat, toate cu EPdfError. Un FontBackend explicit, inclusiv pfbpFreeType, cere Renderer = prpSkia, pentru că PDFium consultă backend-ul de font doar pentru renderer-ul Skia. IsolatePerDocument cere V8Isolate nil, fiindcă PDFium își creează singur isolate-ul per document și pice un CHECK nativ dacă îi pasați și voi unul. Șirurile goale din UserFontPaths sunt respinse. Iar orice apel după prima încercare de încărcare pice cu „PDFium library configuration is already sealed”

Regula din urmă are o consecință practică: nu puteți sonda DLL-ul întâi și să-l configurați apoi. GetSkiaRenderCapabilities, V8FeaturesAvailable, deschiderea unui document și majoritatea celorlalte puncte de intrare apelează LoadLibrary intern, ceea ce sigilează configurația pe loc. Nici apelarea lui UnloadLibrary mai târziu nu o redeschide. Configurați întâi, apoi încărcați, apoi puneți întrebări, ceea ce e exact ordinea pe care ar trebui să o urmeze o rutină de diagnostic:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // o copie, sigur de inspectat
  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;
  // Aceeași rezolvare pe care LoadLibrary a aplicat-o când a construit 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;

Logarea liniei aceleia o singură dată la pornire e ieftină, și e primul lucru pe care îl vreți într-un tichet de suport care spune „textul arată diferit pe server”. PDFium.Loaded e calificat din același motiv ca LoadLibrary: în interiorul unei metode de formă sau de componentă, un Loaded nud se leagă la TComponent.Loaded

Două feluri în care o structură de configurare C versionată iese prost

Fiecare structură de configurare versionată, fie că e FPDF_LIBRARY_CONFIG, o înregistrare Win32 cu cbSize, sau un ABI de plugin, pice în două feluri simetrice, iar un wrapper trebuie să se păzească de ambele. Primul e umplerea unui câmp lăsând versiunea prea mică; al doilea e ridicarea versiunii lăsând un câmp la o valoare zero pe care biblioteca o citește drept alegere deliberată

  1. Câmp setat, versiune prea mică. Scrieți m_BrotliEnabled = 1 într-o structură de versiune 2 și PDFium nu se uită niciodată la el. Apelul reușește, iar stream-urile Brotli rămân nedecodabile. Apărarea e să derivi versiunea din câmpurile efectiv folosite, ceea ce face LoadLibrary, nu s-o hard-codezi
  2. Versiune suficient de mare, câmpul zero înseamnă ceva. Ridicați versiunea la 6 și fiecare câmp până la versiunea 6 e acum viu. FillChar zerorizează m_RendererType la FPDF_RENDERERTYPE_AGG, care e un renderer real, nu „nesetat”. Apărarea e să scrii fiecare câmp acoperit de versiunea aleasă cu o valoare intenționată, și să rezolvi „implicit” contra build-ului efectiv în loc să-l presupui

O a treia regulă decurge pentru valorile care pot bloca apelatul: validați-le contra a ceea ce poate binarul înaintea apelului, folosind cea mai puternică dovadă disponibilă, și fiți sinceri în cod și în documentație când dovada aceea e o euristică. Un simbol exportat e o dovadă. Un nume de crate într-o tabelă de șiruri e o ghicire bună

Referință rapidă: configurația de bibliotecă PDFium Component

  • Apelați ConfigurePdfLibrary o singură dată, înainte ca orice să încarce DLL-ul; orice interogare de capacitate sau încărcare de document îl sigilează
  • Faceți upgrade la v3.123.0 sau mai nou dacă setați BrotliEnabled sau IsolatePerDocument și vă așteptați la output Skia din runtime-urile incluse
  • Lăsați Renderer pe prpDefault dacă nu aveți nevoie de un rasterizator specific; el se rezolvă acum la implicitul build-ului la orice versiune de structură
  • Folosiți PdfNativeRendererType cu GetSkiaRenderCapabilities.PageRender ca să logați ce renderer e efectiv activ
  • Așteptați-vă la EPdfError, nu la un crash, pentru prpSkia pe un DLL doar-AGG sau pfbpFontations pe un DLL non-Fontations în v3.125.0 sau mai nou
  • După o respingere de capacitate, PdfLibraryConfigurationSealed e False și vă puteți reconfigura; după o încărcare de DLL eșuată rămâne True
  • Tratați detecția Fontations drept euristică și păstrați un fallback FreeType
  • Scrieți PDFium.LoadLibrary și PDFium.Loaded cu numele unit-ului ca să evitați ciocnirile de nume cu Win32 și TComponent

Dacă DLL-ul pice înainte ca configurația să conteze chiar deloc, începeți cu diagnosticarea eșecurilor de încărcare a DLL-ului PDFium în Delphi, iar pentru cum găsește componenta binarul potrivit pe fiecare platformă vezi încărcarea bibliotecii native PDFium pe orice țintă. Odată stabilizat renderer-ul, cache-ul de randare și tacticile de zoom fluid acoperă cum ții randarea paginilor rapidă într-un viewer

PDFium Component învelește motorul PDFium pentru Delphi și C++Builder cu verificări de configurație ca astea, deci inițializarea nativă pice drept o excepție Pascal pe care o poți gestiona, nu drept o ieșire de proces. Detalii de produs și descărcări sunt pe pagina de produs PDFium Component pentru Delphi