Î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 |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Scris mereu; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform nu nil |
| 4 | m_RendererType | Renderer altul decât prpDefault |
| 5 | m_FontLibraryType | FontBackend altul decât pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = 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
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 cuRendererlăsat peprpDefault: hashF75B5EB4728ADE87prpAggexplicit: hashF75B5EB4728ADE87, 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ă
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
ConfigurePdfLibraryrefuză 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ștePdfNativeRendererType. 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șiread-fonts(șiread_fonts). Scanarea rulează doar când e cerutpfbpFontations, 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
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ă
- 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 faceLoadLibrary, nu s-o hard-codezi - 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.
FillCharzerorizeazăm_RendererTypelaFPDF_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
ConfigurePdfLibraryo 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
BrotliEnabledsauIsolatePerDocumentși vă așteptați la output Skia din runtime-urile incluse - Lăsați
RendererpeprpDefaultdacă nu aveți nevoie de un rasterizator specific; el se rezolvă acum la implicitul build-ului la orice versiune de structură - Folosiți
PdfNativeRendererTypecuGetSkiaRenderCapabilities.PageRenderca să logați ce renderer e efectiv activ - Așteptați-vă la
EPdfError, nu la un crash, pentruprpSkiape un DLL doar-AGG saupfbpFontationspe un DLL non-Fontations în v3.125.0 sau mai nou - După o respingere de capacitate,
PdfLibraryConfigurationSealede 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șiPDFium.Loadedcu numele unit-ului ca să evitați ciocnirile de nume cu Win32 șiTComponent
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