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 structure | Champ qu’elle ajoute | Réglé par |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Toujours écrit ; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform non nil |
| 4 | m_RendererType | Renderer autre que prpDefault |
| 5 | m_FontLibraryType | FontBackend autre que pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = 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
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 avecRendererlaissé àprpDefault: hashF75B5EB4728ADE87prpAggexplicite : hashF75B5EB4728ADE87, 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
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
ConfigurePdfLibraryrefuse 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’utilisePdfNativeRendererType. 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
skrifaetread-fonts(aussiread_fonts). Le scan ne tourne que quandpfbpFontationsest 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
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é
- 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 faitLoadLibrary, plutôt que d’en coder une en dur - Version assez haute, champ zéro signifie quelque chose. Élevez la version à 6 et chaque champ jusqu’à la version 6 est désormais vivant.
FillCharmetm_RendererTypeà zéro, soitFPDF_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
ConfigurePdfLibraryune 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
BrotliEnabledouIsolatePerDocumentet attendez une sortie Skia des runtimes embarqués - Laissez
RendereràprpDefaultsauf besoin d’un rasterizer précis ; il se résout désormais vers le défaut du build à chaque version de structure - Utilisez
PdfNativeRendererTypeavecGetSkiaRenderCapabilities.PageRenderpour journaliser quel moteur de rendu est réellement actif - Attendez-vous à un
EPdfError, pas à un crash, pourprpSkiasur une DLL AGG uniquement oupfbpFontationssur une DLL sans Fontations en v3.125.0 ou ultérieur - Après un rejet de capacité,
PdfLibraryConfigurationSealedest 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.LoadLibraryetPDFium.Loadedavec le nom d’unité pour éviter les collisions de noms Win32 etTComponent
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