Article technique

Rendu PDF multi-moteur en Delphi : moteurs intégré, Cairo et PDFium avec PDFlibPas

Tout rasteriseur PDF a un fichier qui l'humilie. Dans un dossier de support, il s'agissait d'une série de factures d'énergie où des logos à masque doux sortaient sous forme de rectangles noirs — mais seulement sur le site client, parce que leur build rendait via un autre moteur que le banc de test. Ces bugs se corrigent rarement dans le code applicatif ; la page est valide, le moteur n'en donne simplement pas la même interprétation. Ce qui se corrige, c'est l'architecture : faire du moteur de rendu une décision d'exécution, vérifier quels moteurs le binaire déployé contient réellement, et enregistrer quel moteur a produit chaque image. PDFlibPas, la bibliothèque PDF Delphi et C++Builder de losLab, est conçue exactement pour ce schéma — une surface d'appel de rendu, trois moteurs sélectionnables.

Trois rasteriseurs derrière une seule surface d'appel

La bibliothèque numérote ses moteurs : 1 pour le moteur intégré (par défaut, avec options de lissage GDI+ sous Windows), 2 pour Cairo et 3 pour PDFium, sélectionnés à l'exécution avec SelectRenderer. Les moteurs externes se chargent depuis des DLL dont vous fournissez les chemins via SetCairoFileName et SetPDFiumFileName. Quel que soit le moteur, les mêmes appels réalisent le travail — RenderPageToFile, RenderPageToStream, RenderDocumentToFile — si bien qu'une stratégie de repli change un entier, pas le code de rendu.

Sous le capot, le modèle de destination dépasse les bitmaps : la classe de rendu prend en charge les métafichiers (WMF, EMF, EMF+), EPS, les device contexts directs, les imprimantes et la sortie HTML5, Cairo et PDFium apparaissant comme destinations supplémentaires lorsqu'ils sont compilés. Cet article se concentre sur la sortie raster, où les différences de moteurs sont les plus visibles.

Ne supposez jamais qu'un moteur existe : sondez au démarrage

La prise en charge de Cairo et PDFium relève de la compilation conditionnelle. Un binaire construit sans eux ne lèvera pas d'exception lorsque vous sélectionnez le moteur 2 ou 3 — SelectRenderer renvoie simplement autre chose que l'ID demandé, et si vous ignorez la valeur de retour vous continuez à rendre avec le moteur actif précédent. Le schéma fiable est une sonde au démarrage :

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // engine 1 is always present
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // restore the default before real work
end;

Journalisez le résultat de la sonde avec chaque job. Quand un client signale une différence de rendu, la première question de diagnostic est « quels moteurs votre installation possède-t-elle ? », et une réponse d'une ligne dans le log vaut mieux qu'une session de bureau à distance.

Dix formats de sortie derrière un entier Options

Le paramètre Options des appels de rendu choisit l'encodage de sortie : 0 pour BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+ et 9 HTML5. PNG (5) est le choix raisonnable pour les aperçus et les images de page d'archive ; JPEG (1), associé à SetJPEGQuality, l'emporte pour les scans photographiques où la taille compte.

Un format cache une exigence de flux. Le chemin BMP corrige les champs de résolution de l'en-tête après l'écriture des données image, en revenant à l'offset 0x26 dans la sortie. Rendez du BMP vers un flux en avant seulement — wrapper de compression, flux réseau — et l'appel échoue d'une manière qui ressemble à un problème de moteur alors que ce n'en est pas un. Si une cible non seekable est incontournable, rendez plutôt en PNG, ou passez la sortie BMP par un flux mémoire.

Le DPI transmis n'est pas le DPI obtenu

Tous les appels de rendu prennent un argument DPI, mais la résolution effective est cette valeur multipliée par l'échelle globale de rendu. SetRenderScale vaut 1.0 par défaut ; une fois modifiée, elle s'applique silencieusement à tous les rendus suivants sur cette instance :

PDF.SetRenderScale(2.0);                    // every later render is doubled
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // effectively 300 DPI
PDF.SetRenderScale(1.0);                    // reset, or your thumbnails arrive huge

La même persistance vaut pour SetRenderCropType et le réglage de qualité JPEG. Dans un service qui rend miniatures, aperçus et images prêtes pour l'impression depuis une instance partagée, ces paramètres qui traînent sont la cause des tickets « les miniatures font soudain 40 Mo ». Réinitialisez l'état au début de chaque opération ou dédiez une instance à chaque profil de sortie.

Régler le moteur par défaut avant d'aller chercher un autre

Une part surprenante des demandes « il nous faut un autre moteur » sont en réalité des demandes de réglages qualité. Le moteur intégré expose son comportement de lissage via SetGDIPlusOptions et la famille plus large SetRenderOptions, et il est possible de pointer le moteur vers un runtime GDI+ précis avec SetGDIPlusFileName lorsqu'un environnement de déploiement en embarque un inhabituel. Les dessins au trait crénelés à faible DPI, le texte flou dans les miniatures et les bandes dans les dégradés réagissent à ces boutons — et les ajuster ne coûte rien au déploiement, alors qu'ajouter Cairo ou PDFium à un installeur ajoute des DLL, des variantes de bitness et une obligation de mise à jour.

La séquence pratique pour une plainte de qualité est donc : reproduire d'abord aux DPI et paramètres d'échelle exacts du client, essayer ensuite les options de lissage du moteur intégré, puis seulement comparer la page entre moteurs avec tout le reste constant. Rendez la même page en PNG via les moteurs 1, 2 et 3 avec le même DPI, puis attachez les trois images au ticket. La plupart du temps, deux moteurs sur trois concordent, ce qui indique si l'écart vient de l'interprétation du document ou de l'attente de référence — une preuve qui règle les débats « le rendu est faux » bien plus vite que des rapports de bug fondés sur des adjectifs.

Une chaîne de repli qui s'explique elle-même

Une fois les sondes et la discipline d'état en place, la chaîne de repli elle-même est courte. La détection d'échec utilise LastRenderError, qui porte le texte du message du moteur pour le rendu le plus récent :

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // built-in first
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium as the heavy fallback
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

Deux points de conception sont volontaires. La chaîne journalise la raison de chaque bascule, car « cette page se replie sur PDFium depuis la version 3.7 » est un signal de régression que vous voulez voir en supervision, pas découvrir enterré. Et l'ordre de repli est une politique à choisir par charge de travail : le moteur intégré se déploie sans DLL supplémentaire, ce qui en fait le premier essai logique dans la plupart des installations, tandis que les documents riches en groupes de transparence ou motifs d'ombrage inhabituels sont la raison classique pour laquelle les équipes câblent un moteur alternatif. Mesurez sur votre propre corpus ; le corpus gagne toujours l'argument.

Au-delà des pages isolées : lots TIFF et device contexts vivants

Deux voisins des appels par page complètent l'outillage. RenderAsMultipageTIFFToFile rend une expression de plage de pages directement dans un TIFF multipage — la forme naturelle pour les transmissions d'archives à des systèmes de gestion documentaire antérieurs au PDF. Et RenderPageToDC peint directement sur un device context Windows pour les contrôles d'aperçu, gouverné par son propre trio de réglages persistants (SetRenderDCOffset, SetRenderDCErasePage, plus le type de recadrage) qui méritent la même discipline de réinitialisation que le facteur d'échelle. Le rendu d'aperçu écran et le chemin d'impression ont assez de pièges propres pour avoir un article dédié, lié ci-dessous.

Questions sur le rendu

Pourquoi SelectRenderer(3) renvoie-t-il 0 sur ma machine ? Soit le binaire déployé a été compilé sans prise en charge PDFium, soit SetPDFiumFileName ne pointe pas vers une DLL chargeable — mauvais chemin, mauvaise bitness ou dépendances absentes. Le schéma de sonde au démarrage distingue les deux : si l'appel de nom de fichier renvoie déjà 0, le problème est la DLL.

Quel moteur est le plus rapide ? Il n'existe pas de réponse stable entre types de documents, ce qui est précisément la raison pour laquelle la bibliothèque vous laisse choisir par appel. Benchmarkez chaque moteur sur un échantillon de vos vrais documents, à vos vrais DPI, et recommencez lorsque les DLL de moteur ou le mélange de documents change.

Des pages différentes d'un même document peuvent-elles utiliser des moteurs différents ? Oui. SelectRenderer prend effet pour les appels suivants sur l'instance ; une chaîne de repli peut donc retenter une page récalcitrante sur un autre moteur pendant que le reste du document reste sur le moteur par défaut.

Continuer l'exploration

Pour la peinture d'aperçu, le choix d'imprimante et la gestion DevMode, poursuivez avec l'article sur l'aperçu avant impression et les device contexts. Si vos rendus alimentent un pipeline à haut volume sur de très grands fichiers, l'approche par handle de notre guide Direct Access s'associe bien au rendu par page via DARenderPageToFile.

Le conditionnement des moteurs, les formats pris en charge et les builds d'essai sont détaillés sur la page produit PDFlibPas.