Article technique

Conversion RTF vers PDF en Delphi avec losLab PDF Library

Le RTF circule depuis assez longtemps pour apparaître à des endroits que personne n'avait prévus : générateurs de rapports anciens, pipelines de publipostage, archives de documents juridiques antérieurs aux traitements de texte modernes. Le convertir en PDF à la volée est un besoin récurrent, et l'approche qui marche vraiment sur Windows n'est pas un parseur RTF dédié mais le chemin de rendu que Windows fournit déjà via TRichEdit et EM_FORMATRANGE. la losLab PDF Library expose un context device virtuel 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 vers n'importe quel context device, pas seulement une imprimante physique. Le message EM_FORMATRANGE dit au contrôle de mettre en page une plage de caractères dans un DC donné et renvoie la position du dernier caractère arrivé à tenir. Appelez-le de façon répétée en avançant cpMin à chaque fois, et vous obtenez une sortie page par page. Le GetCanvasDC de la losLab PDF Library fournit un DC en mémoire dimensionné aux dimensions de page que vous précisez ; après avoir rendu une page dedans, LoadFromCanvasDC capture le résultat comme un nouveau document PDF d'une page. Tout le pipeline est là

Une chose à réussir 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 correspondra pas à ce qui se retrouve dans le PDF. Pour une sortie A4, l'approche standard consiste à porter les dimensions en pixels du contrôle à 210 x 297 mm au même DPI que celui que vous passerez plus tard à LoadFromCanvasDC, avant de charger le fichier RTF, avec la même conversion millimètre-pixel que celle qui dimensionne le DC

PDF : pipeline RTF vers PDF : un contrôle Rich Edit dimensionné pagine son texte dans un DC de canevas virtuel via EM_FORMATRANGE et chaque passe est capturée comme une page PDF
EM_FORMATRANGE met une plage de texte RTF dans le DC de canevas virtuel et LoadFromCanvasDC capture le résultat, en répétant jusqu'au dernier caractère atteint

Implémentation Delphi

Ce qui suit utilise l'unité d'import PDFlibAX_TLB, l'import de bibliothèque de types pour l'édition ActiveX de la bibliothèque. La fiche héberge un TRichEdit et un bouton ; le handler OnCreate de la fiche dimensionne le contrôle et charge le RTF, et le clic du 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: IPDFlibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := CoPDFlibrary.Create;
  // Dimensionnez le contrôle au A4 au DPI écran pour que la pagination corresponde au DC
  RichEdit1.Width  := MulDiv(210, Screen.PixelsPerInch * 10, 254);
  RichEdit1.Height := MulDiv(297, Screen.PixelsPerInch * 10, 254);
  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
    // Obtenez un DC virtuel dimensionné A4
    Dc := PdfDoc.GetCanvasDC(
      MulDiv(210, Screen.PixelsPerInch * 10, 254),
      MulDiv(297, Screen.PixelsPerInch * 10, 254));
    // Rendez la page suivante du contenu RTF dans le DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Capturez le contenu du DC comme document PDF
    PdfDoc.LoadFromCanvasDC(Screen.PixelsPerInch, 0);
    PdfDocId := PdfDoc.SelectedDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemoveDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
  // Laissez le contrôle libérer les données de format mises en cache pour le DC
  SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 0, 0);
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  Fr: TFormatRange;
  Dpi, PageWidthTw, PageHeightTw, NextCharPosition: Integer;
begin
  // rc et rcPage sont en twips (1/1440 de pouce), pas en pixels : convertissez
  // la taille de page avec le même DPI qui a dimensionné le DC et le contrôle
  Dpi          := Screen.PixelsPerInch;
  PageWidthTw  := MulDiv(rtfBox.Width,  1440, Dpi);
  PageHeightTw := MulDiv(rtfBox.Height, 1440, Dpi);

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rcPage      := Rect(0, 0, PageWidthTw, PageHeightTw);
  Fr.rc          := Fr.rcPage;  // dessine sur toute la page ; réduisez pour des marges
  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;  // signale la dernière 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 préférez un document unique multipage, AddToFileList plus MergeFileList peuvent assembler les fichiers par page après coup, ou vous pouvez restructurer la boucle pour que chaque nouveau document de page soit plié dans le premier avec MergeDocument. Le pattern SaveToFile suivi de RemoveDocument à chaque itération ci-dessus garde la mémoire de pointe bornée au contenu d'une page, ce qui compte pour de très longs fichiers RTF

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

L'argument DPI de LoadFromCanvasDC dit à la bibliothèque combien de vos pixels font un pouce : la page sort avec une largeur de largeur de canevas divisée par DPI fois 72 points, et de même pour la hauteur. Se tromper et le texte apparaît à la mauvaise taille dans la sortie alors que l'image semble correcte à l'écran

L'unité de rc et rcPage est le détail que la plupart des samples Delphi de cette technique ratent. EM_FORMATRANGE lit les deux rectangles en twips, 1/1440 de pouce, même si le DC dans lequel il dessine travaille en pixels. Remplissez-les avec des valeurs en pixels et le contrôle met le texte en page sur une page quinze fois trop petite à 96 DPI : une poignée de mots par passe, et une boucle qui transforme un document de dix pages en centaines de PDF presque vides. Convertir avec MulDiv(Pixels, 1440, Dpi), en utilisant le DPI qui a dimensionné le DC, remet le saut de page là où le canevas finit réellement. rcPage est la page entière ; rc est la zone que le texte peut occuper, donc les marges viennent du rétrécissement de rc à l'intérieur, en twips également. Quand la boucle finit, un dernier EM_FORMATRANGE avec un lParam nil dit au contrôle de libérer les données de format mises en cache pour le DC

PDF : rectangles rcPage et rc imbriqués pour EM_FORMATRANGE, tous deux donnés en twips convertis depuis les pixels du canevas au DPI qui a dimensionné le DC virtuel
rcPage est toute la page et rc la zone utilisable par le texte ; tous deux partent vers EM_FORMATRANGE en twips, tandis que le DC en dessous reste en pixels

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 embarquées et la plupart des fonctions de formatage de texte enrichi fonctionnent sans traitement supplémentaire, car Rich Edit les rend nativement. La seule zone qui peut surprendre est le texte qui utilise des espacements de paragraphe personnalisés ou des retraits de première ligne exprimés en twips : Rich Edit mesure en twips (1/1440 de pouce), la même unité que les rectangles de TFormatRange, tandis que le DC en dessous travaille en pixels au DPI courant. Le contrôle mappe l'un sur l'autre en dessinant, mais si vous construisez le RTF par programme, vérifiez que vos valeurs de marges sont en twips aussi

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

Sur un affichage en échelle 150 % (144 DPI), la conversion millimètre-pixel rend un compte de pixels plus grand que sur un affichage 100 %. La PDF Library enregistre les dimensions en pixels que vous passez à GetCanvasDC et utilise l'argument DPI de LoadFromCanvasDC pour recalculer la taille physique de page dans le PDF. Tant que la valeur DPI que vous passez est celle qui a servi à convertir les millimètres en pixels, la taille de page en sortie sera correcte quelle que soit l'échelle d'affichage

Si votre application ignore le DPI (l'ancien défaut), Windows met à l'échelle le DC écran et vos calculs en pixels seront faux sur les machines à DPI élevé. Le correctif le plus simple consiste à déclarer la conscience du DPI dans le manifeste de l'application ; l'application reçoit alors les vrais pixels du périphérique, et la valeur Screen.PixelsPerInch que le sample passe à LoadFromCanvasDC rapporte le vrai DPI de l'affichage au lieu de supposer une échelle 100 %

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

La boucle ci-dessus écrit chaque page dans un fichier PDF séparé. Savoir si c'est ce que vous voulez dépend de l'usage aval. Les systèmes de génération de rapports demandent souvent des pages individuelles parce qu'ils assemblent le document final plus tard en fusionnant ou réordonnant des pages. Si vous voulez un PDF unique dès le départ, la bibliothèque permet de le construire en une session : gardez l'ID du document que le premier appel LoadFromCanvasDC crée, et à chaque passe suivante sélectionnez-le avec SelectDocument et appelez MergeDocument avec l'ID du document de la nouvelle page au lieu de SaveToFile, puis enregistrez une fois après la sortie de boucle. Cela évite les fichiers intermédiaires et c'est la bonne structure pour la plupart des scénarios de conversion en document unique

PDF : choisir entre écrire un fichier PDF par passe pour un assemblage ultérieur et construire un PDF multipage unique en ajoutant des pages dans la boucle
La même boucle de rendu alimente soit des fichiers Output séparés soit un document unique qui grandit, en échangeant la flexibilité aval contre les fichiers intermédiaires

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 montrées ici font partie de la losLab PDF Library pour Delphi et C++Builder