Article technique

Conversion RTF vers PDF dans Delphi avec la bibliothèque PDF losLab

Le format RTF existe depuis assez longtemps pour qu'il apparaisse dans des endroits que personne n'avait prévus : générateurs de rapports hérités (legacy report generators), pipelines de publipostage (mail merge), archives de documents juridiques antérieures aux traitements de texte modernes. Sa conversion en PDF à la volée est une exigence récurrente, et l'approche qui fonctionne réellement sous Windows n'est pas un analyseur (parser) RTF dédié mais le chemin de rendu que Windows lui-même fournit déjà via TRichEdit et EM_FORMATRANGE. L'édition DLL de la bibliothèque PDF losLab expose un contexte de périphérique virtuel (virtual device context) qui s'insère directement dans ce pipeline

Le mécanisme : DC virtuel et EM_FORMATRANGE

Les contrôles Rich Edit peuvent paginer leur contenu pour n'importe quel contexte de périphérique, pas seulement une imprimante physique. Le message EM_FORMATRANGE indique au contrôle de disposer (lay out) une plage de caractères dans un DC donné et renvoie la position du dernier caractère qu'il a réussi à faire tenir (managed to fit). Appelez-le de manière répétée, en faisant avancer cpMin à chaque fois, et vous obtenez une sortie page par page. GetCanvasDC de la bibliothèque PDF losLab fournit un DC en mémoire dimensionné aux dimensions de page que vous spécifiez ; après y avoir rendu une page, LoadFromCanvasDc capture le résultat en tant que page PDF. C'est l'ensemble du pipeline

Une chose à bien faire d'emblée : le contrôle TRichEdit doit être dimensionné pour correspondre à la page cible. Si le contrôle est plus petit ou plus grand que les dimensions du DC, la pagination ne s'alignera pas avec ce qui se retrouve dans le PDF. Pour une sortie A4, l'approche standard consiste à définir les dimensions en pixels du contrôle pour correspondre à 210 x 297 mm à 96 DPI avant de charger le fichier RTF, en utilisant les mêmes assistants de mise à l'échelle (scale helpers) que vous utiliserez pour dimensionner le DC

Implémentation Delphi

Ce qui suit utilise l'unité d'importation PDFlibAX_TLB, qui enveloppe (wraps) l'édition DLL de la bibliothèque. Le formulaire (form) héberge un TRichEdit et un bouton ; le gestionnaire (handler) OnCreate du formulaire dimensionne le contrôle et charge le RTF, et le clic sur le bouton pilote la boucle de conversion

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // Size the control to A4 at screen DPI so pagination matches the DC
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // Obtain a virtual DC sized to A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Render the next page of RTF content into the DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Capture the DC contents as a PDF document
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // signals last page
end;

end.

Ce que fait la boucle

PrintRtfBox remplit la structure TFormatRange et la transmet au contrôle Rich Edit via SendMessage. Le contrôle rend les caractères en commençant à cpMin, s'arrêtant lorsque le DC est rempli, et renvoie la position du premier caractère qui ne rentre pas. Lorsque la valeur de retour est égale ou supérieure à la longueur totale du texte, chaque caractère a été rendu et la fonction renvoie zéro, ce qui met fin à la boucle repeat...until

Chaque itération produit un fichier PDF nommé Output1.pdf, Output2.pdf, et ainsi de suite. Si vous souhaitez plutôt un seul document de plusieurs pages, l'API d'ajout de page (page-append API) de la bibliothèque vous permet de les assembler après coup, ou vous pouvez restructurer la boucle pour appeler AddPage au sein d'une session de document unique. Le modèle de SaveToFile par itération suivi de RemovePdfDocument ci-dessus maintient la mémoire de pointe (peak memory) limitée au contenu d'une page, ce qui est important pour les très longs fichiers RTF

Détails de dimensionnement qui font trébucher (trip people up)

L'argument 96 DPI à LoadFromCanvasDc indique à la bibliothèque à quelle résolution d'écran le DC a été rendu, afin qu'elle puisse calculer le mappage point-à-pixel (point-to-pixel mapping) correct pour la page PDF. Si vous vous trompez (Get this wrong), le texte apparaîtra à la mauvaise taille dans la sortie même si l'image semble correcte à l'écran

Le +100 ajouté à RcPage.Right et RcPage.Bottom est une petite marge au-delà du bord visible du contrôle. Rich Edit utilise le rectangle rcPage pour décider où séparer les pages ; sans la marge, une ligne qui tombe exactement à la limite peut être dupliquée sur deux pages. Ce n'est pas une constante magique : vous la voulez suffisamment grande pour que la limite de page tombe proprement à l'intérieur de la zone de disposition (layout area) du contrôle plutôt que sur le dernier pixel

Enfin, le contrôle doit déjà être attaché à une fenêtre de formulaire visible (visible form window) lorsque FormCreate s'exécute afin que sa poignée de fenêtre (window handle) soit valide avant le premier appel à SendMessage. Un TRichEdit créé dynamiquement à l'exécution (runtime) nécessite un appel explicite à HandleNeeded avant le début de la boucle de rendu si le formulaire n'a pas encore été affiché

Gestion des polices et des fonctionnalités RTF

Étant donné que le rendu est effectué par le moteur Windows Rich Edit, la substitution de police suit les mêmes règles que celles qu'il utilise pour l'affichage et l'impression. Les polices référencées dans le fichier RTF qui sont installées sur la machine seront rendues fidèlement ; les polices manquantes seront substituées silencieusement, ce qui peut décaler les longueurs de ligne et la pagination. Pour une conversion par lots en production (production batch conversion), cela vaut la peine de le tester explicitement : chargez un document avec chaque police de caractères (typeface) que vos sources RTF utilisent et confirmez que le nombre de pages de sortie correspond à ce que vous attendez d'un aperçu avant impression manuel

Les tableaux, les images intégrées et la plupart des fonctionnalités de formatage Rich Text fonctionnent sans aucune manipulation supplémentaire car Rich Edit les rend nativement. Le seul domaine qui peut être surprenant est le texte qui utilise un espacement de paragraphe personnalisé ou des retraits de première ligne (first-line indents) exprimés en twips : le système de coordonnées interne de Rich Edit est en twips (1/1440 de pouce), tandis que les coordonnées du DC que vous définissez dans TFormatRange sont en pixels au DPI actuel. Le contrôle se convertit en interne, mais si vous construisez le RTF par programmation, vous devez vérifier que vos valeurs de marge sont dans la bonne unité

Prise en compte du DPI (DPI awareness) et affichages à DPI élevé (high-DPI)

Sur un écran fonctionnant à une échelle de 150% (144 DPI), ScaleX(210, mmPixel) renverra un nombre de pixels plus important que sur un écran à 100%. La bibliothèque PDF enregistre toutes les dimensions de pixels que vous passez à GetCanvasDC et utilise l'argument DPI dans LoadFromCanvasDc pour recalculer à rebours (back-calculate) la taille physique de la page dans le PDF. Tant que la valeur DPI que vous passez correspond au DPI auquel votre application s'exécute, la taille de la page de sortie sera correcte quelle que soit l'échelle de l'affichage

Si votre application ne prend pas en compte le DPI (DPI-unaware) (l'ancienne valeur par défaut), Windows met à l'échelle le DC de l'écran et vos calculs de pixels seront faux sur les machines à DPI élevé. La solution la plus simple consiste à déclarer la prise en compte du DPI dans le manifeste de l'application (application manifest) ; l'application reçoit alors les vrais pixels du périphérique et le 96 que vous passez à LoadFromCanvasDc doit être remplacé par le DPI d'affichage réel obtenu à partir de GetDeviceCaps(GetDC(0), LOGPIXELSX). L'exemple de code ci-dessus code en dur (hardcodes) 96 car il est approprié pour un environnement de mise à l'échelle à 100% et maintient l'exemple court

Structure de sortie : un fichier par page par rapport à un document combiné

La boucle ci-dessus écrit chaque page dans un fichier PDF distinct. Que ce soit ce que vous voulez dépend de l'utilisation en aval (downstream use). Les systèmes de génération de rapports ont souvent besoin de pages individuelles car ils assemblent le document final ultérieurement en fusionnant ou en réorganisant les pages. Si vous voulez un seul PDF dès le départ, la bibliothèque vous permet de créer un document avec plusieurs pages en une seule session : créez le document une fois en dehors de la boucle, appelez la méthode d'ajout de page au lieu de SaveToFile à l'intérieur de la boucle, et enregistrez le document complet après la sortie de la boucle. Cela évite les fichiers intermédiaires et constitue la structure appropriée pour la plupart des scénarios de conversion de document unique

Pour les fichiers RTF volumineux, il vaut la peine d'ajouter un retour de progression (progress feedback) dans la boucle, car le taux de conversion est à peu près proportionnel au nombre de pages et un document de 200 pages peut prendre quelques secondes. La structure repeat...until est facile à étendre : suivez le décalage de caractères dans une mise à jour de la barre de progression (progress bar) après chaque itération, en utilisant LastChar divisé par le nombre total de caractères de RichEdit1.GetTextLen

Les méthodes GetCanvasDC et LoadFromCanvasDc présentées ici font partie de la bibliothèque PDF losLab pour Delphi et C++Builder