Articolo tecnico

Conversione da RTF a PDF in Delphi con losLab PDF Library

Il formato RTF esiste da abbastanza tempo da comparire in luoghi che nessuno aveva previsto: vecchi generatori di report, pipeline di stampa unione (mail merge), archivi di documenti legali antecedenti ai moderni elaboratori di testi. La conversione al volo in PDF è un'esigenza ricorrente, e l'approccio che funziona davvero su Windows non è un parser RTF dedicato ma il percorso di rendering che Windows stesso già fornisce tramite TRichEdit ed EM_FORMATRANGE. L'edizione DLL di losLab PDF Library espone un contesto di dispositivo (device context) virtuale che si inserisce direttamente in quella pipeline

Il meccanismo: DC virtuale ed EM_FORMATRANGE

I controlli Rich Edit possono impaginare il loro contenuto per qualsiasi contesto di dispositivo, non solo per una stampante fisica. Il messaggio EM_FORMATRANGE dice al controllo di disporre un intervallo di caratteri in un determinato DC e restituisce la posizione dell'ultimo carattere che è riuscito a farvi rientrare. Chiamandolo ripetutamente, avanzando cpMin ogni volta, si ottiene un output pagina per pagina. GetCanvasDC di losLab PDF Library fornisce un DC in memoria dimensionato alle dimensioni di pagina specificate; dopo aver eseguito il rendering di una pagina in esso, LoadFromCanvasDc cattura il risultato come una pagina PDF. Questa è l'intera pipeline

Una cosa da sistemare fin da subito: il controllo TRichEdit deve essere dimensionato in modo da corrispondere alla pagina di destinazione. Se il controllo è più piccolo o più grande delle dimensioni del DC, l'impaginazione non sarà allineata con ciò che finisce nel PDF. Per l'output A4, l'approccio standard è impostare le dimensioni in pixel del controllo in modo che corrispondano a 210 x 297 mm a 96 DPI prima di caricare il file RTF, usando gli stessi helper di scala che si useranno per dimensionare il DC

Implementazione in Delphi

Il seguente esempio usa l'unità di importazione PDFlibAX_TLB, che incapsula l'edizione DLL della libreria. Il modulo ospita un TRichEdit e un pulsante; il gestore OnCreate del modulo ridimensiona il controllo e carica l'RTF, mentre il clic sul pulsante guida il ciclo di conversione

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);
  // Dimensiona il controllo in formato A4 ai DPI dello schermo affinché l'impaginazione corrisponda al 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
    // Ottieni un DC virtuale dimensionato per A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Esegui il rendering della successiva pagina di contenuto RTF nel DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Cattura i contenuti del DC come documento PDF
    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;  // segnala l'ultima pagina
end;

end.

Cosa fa il ciclo

PrintRtfBox riempie la struttura TFormatRange e la passa al controllo Rich Edit tramite SendMessage. Il controllo esegue il rendering dei caratteri partendo da cpMin, fermandosi quando il DC si riempie, e restituisce la posizione del primo carattere che non ci è entrato. Quando il valore restituito è uguale o supera la lunghezza totale del testo, significa che è stato eseguito il rendering di ogni carattere e la funzione restituisce zero, il che termina il ciclo repeat...until

Ogni iterazione produce un file PDF denominato Output1.pdf, Output2.pdf, e così via. Se si desidera invece un singolo documento multipagina, l'API della libreria per l'aggiunta di pagine (page-append) consente di assemblarle a posteriori, o è possibile ristrutturare il ciclo per chiamare AddPage all'interno di una sessione di un singolo documento. Lo schema sopra illustrato di SaveToFile per iterazione seguito da RemovePdfDocument mantiene il picco di memoria limitato al contenuto di una singola pagina, il che è importante per i file RTF molto lunghi

Dettagli di dimensionamento che mettono in difficoltà

L'argomento a 96 DPI passato a LoadFromCanvasDc indica alla libreria a quale risoluzione dello schermo è stato renderizzato il DC, in modo da poter calcolare la corretta mappatura punto-pixel per la pagina PDF. Sbagliando questo dato, il testo apparirà di dimensioni errate nell'output anche se l'immagine sembra corretta sullo schermo

Il +100 aggiunto a RcPage.Right e RcPage.Bottom è un piccolo margine oltre il bordo visibile del controllo. Rich Edit usa il rettangolo rcPage per decidere dove dividere le pagine; senza questo margine, una riga che cade esattamente sul limite può essere duplicata su due pagine. Non è una costante magica: dev'essere sufficientemente ampio affinché il confine della pagina cada chiaramente all'interno dell'area di layout del controllo piuttosto che sull'ultimo pixel

Infine, il controllo dev'essere già agganciato a una finestra del modulo visibile quando viene eseguito FormCreate, affinché l'handle della sua finestra sia valido prima della prima chiamata a SendMessage. Un TRichEdit creato dinamicamente a runtime (durante l'esecuzione) necessita di una chiamata esplicita a HandleNeeded prima che inizi il ciclo di rendering, qualora il modulo non fosse stato ancora mostrato

Gestione dei font e delle funzionalità RTF

Poiché il rendering viene eseguito dal motore Rich Edit di Windows, la sostituzione dei font segue le stesse regole che utilizza per la visualizzazione e la stampa. I font citati nel file RTF che sono installati sulla macchina verranno riprodotti fedelmente; i font mancanti verranno sostituiti silenziosamente, il che può spostare le lunghezze delle righe e l'impaginazione. Nelle conversioni batch per la produzione, vale la pena testarlo esplicitamente: carica un documento con ogni carattere utilizzato dalle tue fonti RTF e verifica che il conteggio delle pagine prodotte corrisponda a quello che ti aspetteresti da un'anteprima di stampa manuale

Tabelle, immagini incorporate e la maggior parte delle funzioni di formattazione Rich Text funzionano senza alcuna gestione aggiuntiva perché Rich Edit ne esegue il rendering nativamente. L'unica area che può sorprendere è il testo che utilizza una spaziatura personalizzata dei paragrafi o un rientro della prima riga espresso in twip: il sistema di coordinate interne di Rich Edit è in twip (1/1440 di pollice), mentre le coordinate DC impostate in TFormatRange sono in pixel in base ai DPI attuali. Il controllo esegue la conversione internamente, ma se si sta costruendo l'RTF a livello di codice, occorre verificare che i valori del margine siano nell'unità corretta

Consapevolezza dei DPI e display ad alti DPI

Su uno schermo che lavora con un ridimensionamento del 150% (144 DPI), ScaleX(210, mmPixel) restituirà un conteggio di pixel maggiore rispetto a uno schermo al 100%. PDF Library registra le dimensioni in pixel che passi a GetCanvasDC e usa l'argomento dei DPI in LoadFromCanvasDc per calcolare a ritroso la dimensione fisica della pagina nel PDF. Finché il valore DPI passato corrisponde ai DPI a cui è in esecuzione l'applicazione, la dimensione della pagina di output sarà corretta indipendentemente dal ridimensionamento dello schermo

Se l'applicazione non è compatibile con i DPI (il vecchio standard), Windows ridimensiona il DC dello schermo e i calcoli dei pixel saranno errati sulle macchine ad alti DPI. La soluzione più semplice è dichiarare la compatibilità coi DPI nel file manifest dell'applicazione; a quel punto l'applicazione riceve i veri pixel del dispositivo e il 96 passato a LoadFromCanvasDc dovrebbe essere sostituito con i DPI effettivi dello schermo ottenuti da GetDeviceCaps(GetDC(0), LOGPIXELSX). L'esempio di codice sopra codifica il valore 96 nel codice poiché è appropriato per un ambiente di ridimensionamento al 100% e mantiene l'esempio breve

Struttura dell'output: un file per pagina rispetto a un documento combinato

Il ciclo precedente scrive ogni pagina in un file PDF separato. Se sia ciò che si desidera o meno dipende dall'uso a valle. I sistemi per la generazione di report spesso necessitano di singole pagine perché l'assemblaggio finale del documento avviene in seguito unendo o riordinando le pagine. Se si desidera fin dall'inizio un unico PDF, la libreria permette di creare un documento multipagina in un'unica sessione: si crea il documento una volta fuori dal ciclo, si chiama il metodo di aggiunta pagina al posto di SaveToFile all'interno del ciclo e si salva il documento completo dopo l'uscita dal ciclo. Questo evita i file intermedi ed è la struttura giusta per la maggior parte degli scenari di conversione in un documento singolo

Per file RTF di grandi dimensioni, vale la pena di aggiungere al ciclo una qualche indicazione del progresso, dato che la velocità di conversione è all'incirca proporzionale al numero di pagine e un documento da 200 pagine può richiedere diversi secondi. La struttura repeat...until è facile da estendere: tieni traccia dell'offset dei caratteri nell'aggiornamento di una barra di avanzamento dopo ogni iterazione, usando LastChar diviso per il conteggio totale dei caratteri di RichEdit1.GetTextLen

I metodi GetCanvasDC e LoadFromCanvasDc qui mostrati fanno parte di losLab PDF Library per Delphi e C++Builder