In PDFium Component for Delphi, attivare BrotliEnabled o IsolatePerDocument in TPdfLibraryConfiguration faceva passare la build Skia inclusa al renderer AGG senza alcun errore, perché entrambe le opzioni alzano FPDF_LIBRARY_CONFIG a una versione in cui PDFium legge m_RendererType alla lettera. Dalla v3.123.0 il renderer predefinito resta il default della DLL stessa, e dalla v3.125.0 una richiesta Skia o Fontations che la DLL non può onorare solleva una EPdfError intercettabile anziché uccidere il processo
Nessuno dei due bug si annunciava. Il primo produceva pagine che sembravano a posto, solo renderizzate da un rasterizer diverso, con anti-aliasing e bordi del testo leggermente diversi dalla build che avevi spedito e provato. Il secondo si annunciava, e forte, portando giù il processo host dall'interno dell'inizializzazione nativa. Entrambi vengono dallo stesso posto: una struttura C versionata i cui campi contano solo quando il numero di versione lo dice, e i cui valori zero non sono "non impostato" ma scelte vere
Come decide FPDF_LIBRARY_CONFIG quale renderer usa PDFium?
FPDF_InitLibraryWithConfig consulta m_RendererType solo quando il campo Version della struttura è 4 o superiore, e da quella versione in poi usa il valore esattamente come scritto. Sotto la versione 4 PDFium ignora il campo e prende il default della build, che è Skia nelle build compilate con PDF_USE_SKIA e AGG altrove
Ogni campo successivo segue lo stesso schema. La struttura è cresciuta di una capacità alla volta, e ogni capacità è arrivata insieme a un nuovo numero di versione. PDFium Component costruisce la struttura nativa in LoadLibrary a partire dalla tua TPdfLibraryConfiguration e alza la versione solo fino a dove le opzioni impostate lo richiedono
| Versione della struttura | Campo che aggiunge | Impostato da |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Sempre scritto; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform non nil |
| 4 | m_RendererType | Renderer diverso da prpDefault |
| 5 | m_FontLibraryType | FontBackend diverso da pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
La trappola sta nelle ultime due righe. Le versioni sono cumulative: una struttura versione 6 è anche una struttura versione 4 e versione 5, quindi PDFium legge m_RendererType e m_FontLibraryType anche se hai chiesto solo Brotli. Ciò che si trova in quei due campi in quel momento diventa il renderer e il backend dei font, che tu abbia scelto o no
Perché attivare Brotli scambiava il renderer con AGG?
Prima della v3.123.0, PDFium Component scriveva FPDF_RENDERERTYPE_AGG in m_RendererType per prpDefault, così qualsiasi configurazione che spingeva la struttura a versione 6 o 7 imponeva AGG su una build Skia. I runtime pdfium.dll e pdfium.v8.dll che viaggiano col componente sono build Skia, quindi colpiva il deployment predefinito, non uno esotico
La mappatura sembrava innocua quando è stata scritta. A versione 2 o 3 il campo non viene mai letto, quindi prpDefault voleva davvero dire "quello che fa la DLL". Nel momento in cui BrotliEnabled (versione 6) o IsolatePerDocument (versione 7) sono entrati in scena, lo stesso codice trasformava "nessuna preferenza" in una richiesta AGG esplicita. Niente falliva. PDFium si inizializzava normalmente, renderizzava ogni pagina, e restituiva nessun codice di errore, perché dal suo punto di vista il chiamante aveva chiesto AGG e ricevuto AGG
Un hash dei pixel rende visibile lo scambio dove gli screenshot non arrivano. Renderizzare la prima pagina dello stesso documento campione sotto tre configurazioni dava:
- Configurazione predefinita: hash
502D77C3711B4ACF BrotliEnabled= True conRendererlasciato aprpDefault: hashF75B5EB4728ADE87prpAggesplicito: hashF75B5EB4728ADE87, identico all'esecuzione Brotli
La correzione nella v3.123.0 è la funzione pubblica PdfNativeRendererType, che risolve una TPdfRendererPreference nel valore scritto in m_RendererType. prpAgg e prpSkia si mappano uno a uno. prpDefault ora si mappa su Skia quando la DLL caricata esporta FPDF_RenderPageSkia e su AGG altrimenti. Quell'export è compilato sotto la stessa condizione PDF_USE_SKIA del default Skia stesso, il che lo rende l'unica proprietà della build osservabile da fuori della DLL. Dopo la correzione la configurazione Brotli produce lo stesso hash della predefinita
Il backend dei font non ha mai avuto lo stesso problema. m_FontLibraryType viene letto dalla versione 5 in poi, e il suo valore zero, FPDF_FONTBACKENDTYPE_FREETYPE, è anche il default di PDFium quando il campo non viene letto affatto. Scrivere FreeType per pfbpDefault quindi riproduce esattamente il default nativo. I valori zero non sono sempre sbagliati, semplicemente non sono mai automaticamente giusti
Con la v3.123.0 o successiva, il codice di avvio che scriveresti naturalmente ora fa ciò che dice:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Deve girare prima che qualcosa carichi la libreria nativa
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // alza FPDF_LIBRARY_CONFIG a versione 6
// Il renderer resta prpDefault: risolto a Skia sulle build che esportano
// FPDF_RenderPageSkia e ad AGG sulle build solo-AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Ricorda che BrotliEnabled rende decodificabili gli stream /BrotliDecode del PDF 2.0 solo se la DLL stessa è stata compilata con PDF_ENABLE_BROTLI. Il flag è una richiesta, e su una build senza supporto Brotli non ha effetto. TPdfLibraryConfiguration.Hardened è identica a Default tranne che AllowMachineTime è False, il che blocca il JavaScript del documento dal leggere l'orologio reale; è un punto di partenza ragionevole per l'elaborazione lato server di file non fidati
Che cosa succede quando richiedi un backend che la DLL non contiene?
PDFium non restituisce un errore per un renderer o un backend dei font mancante dalla build: FPDF_InitLibraryWithConfig fa fallire un CHECK nativo, che su Windows emerge come un'eccezione di breakpoint e, senza uno structured exception handler attorno alla chiamata, termina il processo. L'header lo dice chiaramente, avvertendo che un valore non supportato "fallirà in modo simile con un crash immediato"
I due casi concreti sono una build solo-AGG che riceve FPDF_RENDERERTYPE_SKIA, e una build senza Fontations che riceve FPDF_FONTBACKENDTYPE_FONTATIONS. Il runtime Skia incluso è nel secondo gruppo: renderizza con Skia ma usa FreeType per i font. Chiedere prpSkia insieme a pfbpFontations contro di esso produceva External exception 80000003 sul lato Delphi. Quando il debugger o un exception handler per caso lo becca, la situazione resta comunque irrecuperabile:
- PDFium resta mezza-inizializzato
- La configurazione a livello di processo è già sigillata, così
ConfigurePdfLibraryrifiuta una configurazione corretta - Riprovarci con una configurazione diversa nello stesso processo non è più possibile
Questo è il fallimento opposto al bug Brotli. Lì il campo conteneva un valore che nessuno aveva scelto e PDFium lo accettava in silenzio. Qui il campo contiene un valore che il chiamante ha scelto deliberatamente e PDFium non accetta alcuna discussione in proposito. Entrambi sono problemi che un wrapper deve risolvere prima della chiamata nativa, perché dopo non resta più nulla da intercettare
Come precontrolla Skia e Fontations PDFium Component
Dalla v3.125.0, LoadLibrary valida la configurazione dopo aver legato gli export della DLL e prima di chiamare FPDF_InitLibraryWithConfig, e trasforma un renderer o backend font non supportato in una EPdfError con un messaggio che nomina l'impostazione colpevole e le alternative. La DLL viene scaricata e la configurazione viene desigillata, così il chiamante può scegliere altre impostazioni e caricare di nuovo
La decisione in sé vive nella funzione pura PdfLibraryConfigurationSupportError, che prende la configurazione più due booleani che descrivono la build e restituisce una stringa vuota quando la combinazione è sicura. Dato che non tocca alcuno stato nativo, puoi chiamarla dai tuoi test con qualsiasi combinazione di capacità. Dentro LoadLibrary i due booleani vengono da tipi di prove diversi, e meritano livelli di fiducia diversi:
- Skia viene rilevata dalla presenza dell'export
FPDF_RenderPageSkia, lo stesso segnale che usaPdfNativeRendererType. L'export e il renderer Skia sono compilati sotto un'unica condizione, quindi il controllo è esatto - Fontations non ha un export proprio. L'unica traccia che lascia sono le crate font Rust che tira dentro il binario, quindi PDFium Component scansiona il file della libreria caricata in cerca dei nomi di crate
skrifaeread-fonts(ancheread_fonts). La scansione gira solo quando viene richiestopfbpFontations, e un file che non si riesce a leggere conta come "niente Fontations"
Il controllo Fontations è un'euristica, e può sbagliare in una direzione: una build Fontations privata di ognuna di quelle stringhe verrebbe rifiutata benché avrebbe potuto funzionare. Quel compromesso è stato fatto di proposito. Un falso rifiuto ti costa un'eccezione che puoi intercettare e un fallback su FreeType. Una falsa accettazione ti costa il processo
Desigillare conta quanto il controllo. LoadLibrary sigilla la configurazione proprio all'inizio del caricamento, quindi senza il reset un rifiuto di capacità lascerebbe ConfigurePdfLibrary a rispondere a ogni retry con EPdfError "PDFium library configuration is already sealed". Il percorso di rifiuto chiama prima UnloadLibrary; la sua chiamata FPDF_DestroyLibrary è sicura in quel punto perché PDFium non è ancora stato inizializzato e restituisce subito. Gli altri fallimenti di caricamento, come una DLL mancante o una mancata corrispondenza di architettura, conservano il sigillo, quindi un loop di retry deve saper distinguere i due casi:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // qualificato per unità: Windows.LoadLibrary ha lo stesso nome
Result := prpSkia;
except
on E: EPdfError do
begin
// Un rifiuto di capacità scarica la DLL e desigilla la configurazione.
// Una DLL che proprio non si è caricata resta sigillata: riprovare non aiuta
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Nota il PDFium.LoadLibrary esplicito. In un'unità che usa anche Windows o Winapi.Windows, una LoadLibrary non qualificata si risolve sull'unità che compare per ultima nella uses clause; quando è la funzione Win32, la chiamata senza parametri non compila con un errore di conteggio argomenti che non dice nulla su PDFium
Validazione che avviene ancora prima
ConfigurePdfLibrary rifiuta alcune combinazioni prima che entri in gioco qualsiasi DLL, tutte con EPdfError. Un FontBackend esplicito, pfbpFreeType compreso, richiede Renderer = prpSkia, perché PDFium consulta il backend dei font solo per il renderer Skia. IsolatePerDocument richiede che V8Isolate sia nil, dato che PDFium crea il suo isolate per documento e fa fallire un CHECK nativo se gliene passi anche uno tuo. Le stringhe vuote in UserFontPaths vengono rifiutate. E qualsiasi chiamata dopo il primo tentativo di caricamento fallisce con "PDFium library configuration is already sealed"
Quell'ultima regola ha una conseguenza pratica: non puoi sbirciare prima la DLL e configurarla dopo. GetSkiaRenderCapabilities, V8FeaturesAvailable, l'apertura di un documento e la maggior parte degli altri punti d'ingresso chiamano LoadLibrary internamente, il che sigilla la configurazione sul posto. Nemmeno chiamare UnloadLibrary dopo la riapre. Prima si configura, poi si carica, poi si fanno le domande, che è esattamente l'ordine che una routine diagnostica dovrebbe seguire:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // una copia, sicura da ispezionare
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;
// Stessa risoluzione che LoadLibrary ha applicato costruendo 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;
Registrare quella riga una volta all'avvio costa poco, ed è la prima cosa che vuoi in un ticket di supporto che dice "il testo sembra diverso sul server". PDFium.Loaded è qualificato per lo stesso motivo di LoadLibrary: dentro un metodo di form o componente, una Loaded nuda si lega a TComponent.Loaded
Due modi in cui una struct di configurazione C versionata va storta
Ogni struttura di configurazione versionata, che sia FPDF_LIBRARY_CONFIG, un record Win32 con cbSize, o l'ABI di un plugin, fallisce in due modi simmetrici, e un wrapper deve guardarsi da entrambi. Il primo è riempire un campo lasciando la versione troppo bassa; il secondo è alzare la versione lasciando un campo a un valore zero che la libreria legge come scelta deliberata
- Campo impostato, versione troppo bassa. Scrivere
m_BrotliEnabled= 1 in una struttura versione 2 e PDFium non lo guarda mai. La chiamata riesce e gli stream Brotli restano indecifrabili. La difesa è ricavare la versione dai campi effettivamente in uso, che è ciò che faLoadLibrary, anziché scriverne uno a mano - Versione abbastanza alta, campo zero significa qualcosa. Alza la versione a 6 e ogni campo fino alla versione 6 è ora vivo.
FillCharazzeram_RendererTypeaFPDF_RENDERERTYPE_AGG, che è un renderer vero, non "non impostato". La difesa è scrivere ogni campo che la versione scelta copre con un valore intenzionale, e risolvere "default" contro la build reale anziché presumere
Una terza regola discende per i valori che possono far crashare il chiamato: validali contro ciò che il binario sa fare prima della chiamata, usando la prova più forte disponibile, e sii onesto nel codice e nella documentazione quando quella prova è un'euristica. Un simbolo esportato è una prova. Un nome di crate in una tabella di stringhe è un buon indizio
Riferimento rapido: configurazione della libreria PDFium Component
- Chiama
ConfigurePdfLibraryuna volta sola, prima che qualsiasi cosa carichi la DLL; qualsiasi query di capacità o caricamento documento la sigilla - Aggiorna alla v3.123.0 o successiva se imposti
BrotliEnabledoIsolatePerDocumente ti aspetti output Skia dai runtime inclusi - Lascia
RendereraprpDefaulta meno che tu non abbia bisogno di un rasterizer specifico; ora si risolve al default della build a ogni versione della struttura - Usa
PdfNativeRendererTypeconGetSkiaRenderCapabilities.PageRenderper registrare quale renderer è realmente attivo - Aspettati una
EPdfError, non un crash, perprpSkiasu una DLL solo-AGG opfbpFontationssu una DLL senza Fontations nella v3.125.0 o successiva - Dopo un rifiuto di capacità,
PdfLibraryConfigurationSealedè False e puoi riconfigurare; dopo un caricamento DLL fallito resta True - Tratta il rilevamento Fontations come un'euristica e tieni pronto un fallback FreeType
- Scrivi
PDFium.LoadLibraryePDFium.Loadedcon il nome dell'unità per evitare collisioni di nome con Win32 eTComponent
Se la DLL fallisce prima ancora che la configurazione conti qualcosa, parti da diagnosticare i fallimenti di caricamento della DLL PDFium in Delphi, e per come il componente trova il binario giusto su ogni piattaforma vedi caricare la libreria nativa PDFium su qualsiasi target. Una volta sistemato il renderer, cache di render e tattiche per lo zoom fluido copre come tenere veloce il rendering delle pagine in un viewer
PDFium Component incapsula il motore PDFium per Delphi e C++Builder con controlli di configurazione come questi, così l'inizializzazione nativa fallisce come un'eccezione Pascal che puoi gestire anziché come un'uscita dal processo. Dettagli prodotto e download sono sulla pagina prodotto di PDFium Component for Delphi