Article technique

Config PDFium : quand Brotli échange Skia en silence

Dans PDFium Component pour Delphi, activer BrotliEnabled ou IsolatePerDocument dans TPdfLibraryConfiguration faisait passer le build Skia embarqué au moteur de rendu AGG sans aucune erreur, parce que les deux options élèvent FPDF_LIBRARY_CONFIG à une version où PDFium lit m_RendererType au pied de la lettre. Depuis v3.123.0, le moteur de rendu par défaut reste celui par défaut de la DLL, et depuis v3.125.0, une demande Skia ou Fontations que la DLL ne peut pas honorer lève un EPdfError rattrapable au lieu de tuer le processus

Aucun des deux bugs ne s’est annoncé. Le premier produisait des pages qui avaient l’air bonnes, simplement rendues par un autre rasterizer, avec un anticrénelage et des bords de texte légèrement différents du build que vous avez livré et testé. Le second s’est annoncé, fort et clair, en emportant le processus hôte depuis l’intérieur de l’initialisation native. Les deux viennent du même endroit : une structure C versionnée dont les champs ne comptent qu’une fois que le numéro de version le dit, et dont les valeurs zéro ne sont pas des « non réglé » mais de vrais choix

Comment FPDF_LIBRARY_CONFIG décide-t-il quel moteur de rendu PDFium utilise ?

FPDF_InitLibraryWithConfig ne consulte m_RendererType que quand le champ Version de la structure vaut 4 ou plus, et à partir de cette version il utilise la valeur exactement telle qu’écrite. En dessous de la version 4, PDFium ignore le champ et prend le défaut du build, qui est Skia dans les builds compilés avec PDF_USE_SKIA et AGG partout ailleurs

Chaque champ ultérieur suit le même schéma. La structure a grandi une capacité à la fois, et chaque capacité est arrivée accompagnée d’un nouveau numéro de version. PDFium Component construit la structure native dans LoadLibrary à partir de votre TPdfLibraryConfiguration et n’élève la version qu’autant que les options que vous réglez l’exigent

Version de structureChamp qu’elle ajouteRéglé par
2m_pIsolate, m_v8EmbedderSlotToujours écrit ; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform non nil
4m_RendererTypeRenderer autre que prpDefault
5m_FontLibraryTypeFontBackend autre que pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Le piège est dans les deux dernières lignes. Les versions sont cumulatives : une structure de version 6 est aussi une structure de version 4 et de version 5, donc PDFium lit m_RendererType et m_FontLibraryType même si vous n’avez demandé que Brotli. Ce qui siège dans ces deux champs à ce moment devient le moteur de rendu et le backend de polices, que vous ayez voulu les choisir ou non

Échelle de versions FPDF_LIBRARY_CONFIG de PDFium Component de la version 2 à la version 7 montrant quelle option TPdfLibraryConfiguration ajoute m_RendererType, m_FontLibraryType, m_BrotliEnabled et m_IsolatePerDocument, et pourquoi les versions cumulatives font d’un champ moteur à zéro un choix AGG délibéré plutôt qu’une valeur non réglée sur tout build
Chaque option élève la version de la structure et chaque champ antérieur reste vivant, donc le zéro de m_RendererType arrive à PDFium comme une demande AGG explicite

Pourquoi activer Brotli commutait-il le moteur de rendu vers AGG ?

Avant v3.123.0, PDFium Component écrivait FPDF_RENDERERTYPE_AGG dans m_RendererType pour prpDefault, donc toute configuration qui poussait la structure en version 6 ou 7 imposait AGG à un build Skia. Les runtimes pdfium.dll et pdfium.v8.dll livrés avec le composant sont des builds Skia, donc cela touchait le déploiement par défaut, pas un déploiement exotique

La correspondance avait l’air inoffensive au moment où elle a été écrite. En version 2 ou 3, le champ n’est jamais lu, donc prpDefault voulait bien dire « ce que fait la DLL ». Dès que BrotliEnabled (version 6) ou IsolatePerDocument (version 7) est entré en scène, le même code a transformé « sans préférence » en demande AGG explicite. Rien n’a échoué. PDFium s’est initialisé normalement, a rendu chaque page, et n’a renvoyé aucun code d’erreur, parce que de son point de vue l’appelant avait demandé AGG et avait reçu AGG

Un hash de pixels rend l’échange visible là où les captures d’écran ne le font pas. Rendre la première page du même document d’exemple sous trois configurations donnait :

  • Configuration par défaut : hash 502D77C3711B4ACF
  • BrotliEnabled = True avec Renderer laissé à prpDefault : hash F75B5EB4728ADE87
  • prpAgg explicite : hash F75B5EB4728ADE87, identique à la course Brotli

Le correctif de v3.123.0 est la fonction publique PdfNativeRendererType, qui résout un TPdfRendererPreference vers la valeur écrite dans m_RendererType. prpAgg et prpSkia se correspondent un à un. prpDefault se résout désormais vers Skia quand la DLL chargée exporte FPDF_RenderPageSkia, et vers AGG sinon. Cet export est compilé sous la même condition PDF_USE_SKIA que le défaut Skia lui-même, ce qui en fait la seule propriété de build observable depuis l’extérieur de la DLL. Après le correctif, la configuration Brotli produit le même hash que la configuration par défaut

Comparaison de hash de pixels PDFium Component montrant le hash de rendu Skia par défaut 502D77C3711B4ACF, la configuration BrotliEnabled d’avant v3.123.0 coïncidant avec une course prpAgg explicite au hash F75B5EB4728ADE87, et le wrapper corrigé résolvant prpDefault via l’export FPDF_RenderPageSkia vers le hash Skia d’origine
Un hash de pixels attrape ce que les captures d’écran cachent : activer Brotli rendait chaque page avec AGG, et le défaut corrigé colle désormais à la configuration non touchée

Le backend de polices n’a jamais eu le même problème. m_FontLibraryType est lu à partir de la version 5, et sa valeur zéro, FPDF_FONTBACKENDTYPE_FREETYPE, est aussi le défaut de PDFium quand le champ n’est pas lu du tout. Écrire FreeType pour pfbpDefault reproduit donc exactement le défaut natif. Les valeurs zéro ne sont pas toujours fausses, elles ne sont juste jamais automatiquement justes

Avec v3.123.0 ou ultérieur, le code de démarrage que vous écririez naturellement fait désormais ce qu’il dit :

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // À exécuter avant que quoi que ce soit ne charge la bibliothèque native
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // élève FPDF_LIBRARY_CONFIG à la version 6
  // Renderer reste prpDefault : résolu vers Skia sur les builds qui exportent
  // FPDF_RenderPageSkia et vers AGG sur les builds AGG uniquement
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Rappelez-vous que BrotliEnabled ne rend les flux /BrotliDecode du PDF 2.0 décodables que si la DLL elle-même a été compilée avec PDF_ENABLE_BROTLI. Le drapeau est une demande, et sur un build sans support Brotli il n’a aucun effet. TPdfLibraryConfiguration.Hardened égale Default sauf que AllowMachineTime est False, ce qui empêche le JavaScript du document de lire l’horloge réelle ; c’est un point de départ raisonnable pour le traitement côté serveur de fichiers non fiables

Que se passe-t-il quand vous demandez un backend que la DLL ne contient pas ?

PDFium ne renvoie pas d’erreur pour un moteur de rendu ou un backend de polices absent du build : FPDF_InitLibraryWithConfig fait échouer un CHECK natif, qui sous Windows se manifeste comme une exception de breakpoint et, sans gestionnaire d’exceptions structuré autour de l’appel, termine le processus. L’en-tête le dit lui-même, en avertissant qu’une valeur non supportée « échouera de la même façon avec un crash immédiat »

Les deux cas concrets sont un build AGG uniquement qui reçoit FPDF_RENDERERTYPE_SKIA, et un build sans Fontations qui reçoit FPDF_FONTBACKENDTYPE_FONTATIONS. Le runtime Skia embarqué est dans le second groupe : il rend avec Skia mais utilise FreeType pour les polices. Demander prpSkia avec pfbpFontations contre lui produisait External exception 80000003 côté Delphi. Quand le débogueur ou un gestionnaire d’exceptions arrive à l’attraper, la situation reste de toute façon irrécupérable :

  • PDFium reste à moitié initialisé
  • La configuration à l’échelle du processus est déjà scellée, donc ConfigurePdfLibrary refuse une configuration corrigée
  • Réessayer avec une configuration différente dans le même processus n’est plus possible

C’est la défaillance opposée au bug Brotli. Là, le champ tenait une valeur que personne n’avait choisie et PDFium l’acceptait en silence. Ici, le champ tient une valeur que l’appelant a choisie délibérément et PDFium n’accepte aucune discussion à son sujet. Ce sont deux problèmes qu’un wrapper doit régler avant l’appel natif, parce qu’après il ne reste plus rien à attraper

Comment PDFium Component prévérifie Skia et Fontations

Depuis v3.125.0, LoadLibrary valide la configuration après la liaison des exports de la DLL et avant d’appeler FPDF_InitLibraryWithConfig, et transforme un moteur de rendu ou un backend de polices non supporté en EPdfError avec un message qui nomme le réglage fautif et les alternatives. La DLL est déchargée et la configuration est déscellée, si bien que l’appelant peut choisir d’autres réglages et charger à nouveau

La décision elle-même vit dans la fonction pure PdfLibraryConfigurationSupportError, qui prend la configuration plus deux booléens décrivant le build et renvoie une chaîne vide quand la combinaison est sûre. Comme elle ne touche aucun état natif, vous pouvez l’appeler depuis vos propres tests avec n’importe quelle combinaison de capacités. À l’intérieur de LoadLibrary, les deux booléens viennent de sortes de preuves différentes, et ils méritent des niveaux de confiance différents :

  • Skia est détecté d’après la présence de l’export FPDF_RenderPageSkia, le même signal qu’utilise PdfNativeRendererType. L’export et le moteur de rendu Skia sont compilés sous une même condition, donc la vérification est exacte
  • Fontations n’a aucun export à lui. Sa seule trace, ce sont les crates Rust de polices qu’il tire dans le binaire, donc PDFium Component scanne le fichier de bibliothèque chargé à la recherche des noms de crates skrifa et read-fonts (aussi read_fonts). Le scan ne tourne que quand pfbpFontations est demandé, et un fichier illisible compte comme « pas de Fontations »

La vérification Fontations est une heuristique, et elle peut se tromper dans un sens : un build Fontations strippé de chacune de ces chaînes serait rejeté alors qu’il aurait pu marcher. Ce compromis a été fait à dessein. Un faux rejet vous coûte une exception que vous pouvez attraper et un repli vers FreeType. Une fausse acceptation vous coûte le processus

Désceller compte autant que la vérification. LoadLibrary scelle la configuration tout au début du chargement, donc sans la remise à zéro, un rejet de capacité laisserait ConfigurePdfLibrary répondre à chaque réessai par EPdfError « la configuration de la bibliothèque PDFium est déjà scellée ». Le chemin de rejet appelle d’abord UnloadLibrary ; son appel FPDF_DestroyLibrary est sûr à ce moment parce que PDFium n’a pas encore été initialisé et renvoie immédiatement. Les autres échecs de chargement, tels qu’une DLL manquante ou une incompatibilité d’architecture, gardent le sceau, donc une boucle de réessai doit distinguer les deux :

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // qualifié par unité : Windows.LoadLibrary porte le même nom
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Un rejet de capacité décharge la DLL et déscelle la configuration.
      // Une DLL qui n’a jamais chargé reste scellée : réessayer ne peut rien
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Notez le PDFium.LoadLibrary explicite. Dans une unité qui utilise aussi Windows ou Winapi.Windows, un LoadLibrary non qualifié se résout vers l’unité qui apparaît en dernier dans la clause uses ; quand c’est la fonction Win32, l’appel sans paramètre échoue à la compilation avec une erreur de compte d’arguments qui ne dit rien de PDFium

Flux de prévérification LoadLibrary de PDFium Component où ConfigurePdfLibrary scelle la configuration, la vérification de capacité teste l’export FPDF_RenderPageSkia et les preuves de chaînes skrifa, une demande non supportée lève un EPdfError rattrapable et déscelle pour réessayer, tandis qu’une DLL qui ne charge jamais garde PdfLibraryConfigurationSealed true
La validation tourne après la liaison des exports et avant l’initialisation, donc un backend absent échoue en EPdfError rattrapable au lieu d’un CHECK natif qui tue le processus

Une validation qui intervient encore plus tôt

ConfigurePdfLibrary refuse certaines combinaisons avant qu’aucune DLL n’intervienne, toutes avec EPdfError. Un FontBackend explicite, pfbpFreeType compris, exige Renderer = prpSkia, parce que PDFium ne consulte le backend de polices que pour le moteur de rendu Skia. IsolatePerDocument exige que V8Isolate soit nil, puisque PDFium crée son propre isolat par document et fait échouer un CHECK natif si vous lui en donnez aussi un. Les chaînes vides dans UserFontPaths sont rejetées. Et tout appel après la première tentative de chargement échoue avec « la configuration de la bibliothèque PDFium est déjà scellée »

Cette dernière règle a une conséquence pratique : vous ne pouvez pas sonder la DLL d’abord et la configurer ensuite. GetSkiaRenderCapabilities, V8FeaturesAvailable, l’ouverture d’un document et la plupart des autres points d’entrée appellent LoadLibrary en interne, ce qui scelle la configuration sur le champ. Appeler UnloadLibrary ensuite ne la rouvre pas non plus. Configurez d’abord, puis chargez, puis posez vos questions, exactement l’ordre qu’une routine de diagnostic doit suivre :

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // une copie, sûre à inspecter
  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;
  // La même résolution que LoadLibrary a appliquée en construisant 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;

Journaliser cette ligne une fois au démarrage ne coûte rien, et c’est la première chose que vous voulez dans un ticket de support qui dit « le texte a l’air différent sur le serveur ». PDFium.Loaded est qualifié pour la même raison que LoadLibrary : dans une méthode de fiche ou de composant, un Loaded nu se lie à TComponent.Loaded

Deux façons dont une structure de configuration C versionnée tourne mal

Toute structure de configuration versionnée, que ce soit FPDF_LIBRARY_CONFIG, un enregistrement Win32 à cbSize, ou un ABI de plugin, échoue de deux façons symétriques, et un wrapper doit se garder des deux. La première, c’est remplir un champ en laissant la version trop basse ; la seconde, c’est élever la version en laissant un champ à une valeur zéro que la bibliothèque lit comme un choix délibéré

  1. Champ réglé, version trop basse. Écrivez m_BrotliEnabled = 1 dans une structure de version 2 et PDFium ne le regarde jamais. L’appel réussit et les flux Brotli restent indécodables. La défense consiste à dériver la version des champs réellement utilisés, ce que fait LoadLibrary, plutôt que d’en coder une en dur
  2. Version assez haute, champ zéro signifie quelque chose. Élevez la version à 6 et chaque champ jusqu’à la version 6 est désormais vivant. FillChar met m_RendererType à zéro, soit FPDF_RENDERERTYPE_AGG, un vrai moteur de rendu, pas un « non réglé ». La défense consiste à écrire chaque champ couvert par la version choisie avec une valeur intentionnelle, et à résoudre « défaut » contre le build réel au lieu de le présumer

Une troisième règle suit pour les valeurs qui peuvent faire planter l’appelé : validez-les contre ce que le binaire sait faire avant l’appel, avec la preuve la plus solide disponible, et soyez honnête dans le code et la documentation quand cette preuve est une heuristique. Un symbole exporté est une preuve. Un nom de crate dans une table de chaînes est une bonne devinette

Aide-mémoire : la configuration de bibliothèque de PDFium Component

  • Appelez ConfigurePdfLibrary une fois, avant que quoi que ce soit ne charge la DLL ; toute requête de capacité ou chargement de document la scelle
  • Passez à v3.123.0 ou ultérieur si vous réglez BrotliEnabled ou IsolatePerDocument et attendez une sortie Skia des runtimes embarqués
  • Laissez Renderer à prpDefault sauf besoin d’un rasterizer précis ; il se résout désormais vers le défaut du build à chaque version de structure
  • Utilisez PdfNativeRendererType avec GetSkiaRenderCapabilities.PageRender pour journaliser quel moteur de rendu est réellement actif
  • Attendez-vous à un EPdfError, pas à un crash, pour prpSkia sur une DLL AGG uniquement ou pfbpFontations sur une DLL sans Fontations en v3.125.0 ou ultérieur
  • Après un rejet de capacité, PdfLibraryConfigurationSealed est False et vous pouvez reconfigurer ; après un échec de chargement de DLL il reste True
  • Traitez la détection Fontations comme une heuristique et gardez un repli FreeType
  • Écrivez PDFium.LoadLibrary et PDFium.Loaded avec le nom d’unité pour éviter les collisions de noms Win32 et TComponent

Si la DLL échoue avant même que la configuration ne compte, commencez par le diagnostic des échecs de chargement de la DLL PDFium en Delphi, et pour la façon dont le composant trouve le bon binaire sur chaque plateforme, voir le chargement de la bibliothèque native PDFium sur n’importe quelle cible. Une fois le moteur de rendu réglé, cache de rendu et tactiques de zoom fluide couvre comment garder le rendu de pages rapide dans un visualiseur

PDFium Component enveloppe le moteur PDFium pour Delphi et C++Builder avec des vérifications de configuration comme celles-ci, si bien que l’initialisation native échoue en une exception Pascal que vous pouvez traiter plutôt qu’en sortie de processus. Détails produit et téléchargements sont sur la page produit PDFium Component for Delphi