Articolo tecnico

Config libreria PDFium: quando Brotli scambia Skia

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 strutturaCampo che aggiungeImpostato da
2m_pIsolate, m_v8EmbedderSlotSempre scritto; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform non nil
4m_RendererTypeRenderer diverso da prpDefault
5m_FontLibraryTypeFontBackend diverso da pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = 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

Scala di versioni FPDF_LIBRARY_CONFIG di PDFium Component dalla versione 2 alla versione 7 che mostra quale opzione TPdfLibraryConfiguration aggiunge m_RendererType, m_FontLibraryType, m_BrotliEnabled e m_IsolatePerDocument, e perché le versioni cumulative rendono un campo renderer azzerato una scelta AGG deliberata anziché un valore non impostato su qualsiasi build
Ogni opzione alza la versione della struttura e ogni campo precedente resta vivo, così lo zero in m_RendererType arriva a PDFium come una richiesta AGG esplicita

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 con Renderer lasciato a prpDefault: hash F75B5EB4728ADE87
  • prpAgg esplicito: hash F75B5EB4728ADE87, 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

Confronto di hash di pixel PDFium Component che mostra l'hash del render Skia predefinito 502D77C3711B4ACF, la configurazione BrotliEnabled precedente alla v3.123.0 combaciante con un'esecuzione prpAgg esplicita con hash F75B5EB4728ADE87, e il wrapper corretto che risolve prpDefault attraverso l'export FPDF_RenderPageSkia tornando all'hash Skia originale
Un hash di pixel becca ciò che gli screenshot nascondono: attivare Brotli renderizzava ogni pagina con AGG, e il default corretto ora combacia con la configurazione intoccata

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ì ConfigurePdfLibrary rifiuta 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 usa PdfNativeRendererType. 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 skrifa e read-fonts (anche read_fonts). La scansione gira solo quando viene richiesto pfbpFontations, 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

Flusso di precontrollo LoadLibrary di PDFium Component in cui ConfigurePdfLibrary sigilla la configurazione, il controllo di capacità testa l'export FPDF_RenderPageSkia e le prove di stringa skrifa, una richiesta non supportata solleva una EPdfError intercettabile e desigilla per il retry, mentre una DLL che non si carica mai tiene PdfLibraryConfigurationSealed true
La validazione gira dopo che gli export si sono legati e prima dell'inizializzazione, così un backend mancante fallisce come una EPdfError che puoi intercettare anziché come un CHECK nativo che uccide il processo

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

  1. 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 fa LoadLibrary, anziché scriverne uno a mano
  2. Versione abbastanza alta, campo zero significa qualcosa. Alza la versione a 6 e ogni campo fino alla versione 6 è ora vivo. FillChar azzera m_RendererType a FPDF_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 ConfigurePdfLibrary una 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 BrotliEnabled o IsolatePerDocument e ti aspetti output Skia dai runtime inclusi
  • Lascia Renderer a prpDefault a meno che tu non abbia bisogno di un rasterizer specifico; ora si risolve al default della build a ogni versione della struttura
  • Usa PdfNativeRendererType con GetSkiaRenderCapabilities.PageRender per registrare quale renderer è realmente attivo
  • Aspettati una EPdfError, non un crash, per prpSkia su una DLL solo-AGG o pfbpFontations su 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.LoadLibrary e PDFium.Loaded con il nome dell'unità per evitare collisioni di nome con Win32 e TComponent

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