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. Convertirlo 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. 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. Il GetCanvasDC di losLab PDF Library fornisce un DC in memoria dimensionato alle dimensioni di pagina che specificate; dopo aver eseguito il rendering di una pagina in esso, LoadFromCanvasDC cattura il risultato come un nuovo documento PDF di una pagina. 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 agli stessi DPI che passerete poi a LoadFromCanvasDC, prima di caricare il file RTF, con la stessa conversione millimetro-pixel che usate per dimensionare il DC

PDF: pipeline da RTF a PDF: un controllo Rich Edit dimensionato pagina il suo testo in un DC canvas virtuale attraverso EM_FORMATRANGE e ogni passata viene catturata come una pagina PDF
EM_FORMATRANGE compone un intervallo di testo RTF nel DC canvas virtuale e LoadFromCanvasDC cattura il risultato, ripetendo finché non si raggiunge l'ultimo carattere

Implementazione in Delphi

Il seguente esempio usa l'unità di importazione PDFlibAX_TLB, l'import della type library per l'edizione ActiveX 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: IPDFlibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := CoPDFlibrary.Create;
  // Dimensiona il controllo ad A4 ai DPI dello schermo così l'impaginazione combacia con il 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
    // Ottieni un DC virtuale dimensionato per A4
    Dc := PdfDoc.GetCanvasDC(
      MulDiv(210, Screen.PixelsPerInch * 10, 254),
      MulDiv(297, Screen.PixelsPerInch * 10, 254));
    // 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(Screen.PixelsPerInch, 0);
    PdfDocId := PdfDoc.SelectedDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemoveDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
  // Lascia che il controllo rilasci i dati di formattazione che ha messo in cache per il 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 e rcPage sono in twips (1/1440 di pollice), non in pixel: converti
  // le dimensioni di pagina con gli stessi DPI che hanno dimensionato DC e controllo
  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;  // disegna su tutta la pagina; restringi per i margini
  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, AddToFileList più MergeFileList possono assemblare i file per pagina a posteriori, oppure si può ristrutturare il ciclo così che ogni nuovo documento di pagina venga fuso nel primo con MergeDocument. Lo schema sopra illustrato di SaveToFile per iterazione seguito da RemoveDocument 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 DPI di LoadFromCanvasDC dice alla libreria quanti dei vostri pixel fanno un pollice: la pagina esce larga quanto la larghezza del canvas divisa per i DPI per 72 punti, e lo stesso per l'altezza. Sbagliando questo dato, il testo apparirà di dimensioni errate nell'output anche se l'immagine sembra corretta sullo schermo

L'unità di rc e rcPage è il dettaglio che la maggior parte degli esempi Delphi di questa tecnica sbaglia. EM_FORMATRANGE legge entrambi i rettangoli in twips, 1/1440 di pollice, anche se il DC in cui disegna lavora in pixel. Riempiteli con valori in pixel e il controllo compone il testo su una pagina quindici volte troppo piccola a 96 DPI: una manciata di parole per passata, e un ciclo che trasforma un documento di dieci pagine in centinaia di PDF quasi vuoti. Convertire con MulDiv(Pixels, 1440, Dpi), usando i DPI che hanno dimensionato il DC, mette l'interruzione di pagina dove il canvas finisce davvero. rcPage è l'intera pagina; rc è l'area che il testo può occupare, quindi i margini vengono dal restringere rc dentro di essa, sempre in twips. Quando il ciclo finisce, un altro EM_FORMATRANGE con lParam nil dice al controllo di liberare i dati di formattazione che ha messo in cache per il DC

PDF: rettangoli rcPage e rc nidificati per EM_FORMATRANGE, entrambi espressi in twips convertiti dai pixel del canvas ai DPI che hanno dimensionato il DC virtuale
rcPage è l'intera pagina e rc l'area che il testo può usare; entrambe arrivano a EM_FORMATRANGE in twips, mentre il DC sottostante resta in 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 espressi in twip: Rich Edit misura in twip (1/1440 di pollice), la stessa unità dei rettangoli in TFormatRange, mentre il DC sottostante lavora in pixel ai DPI correnti. Il controllo mappa l'uno sull'altra mentre disegna, ma se state costruendo l'RTF a livello di codice dovete verificare che anche i vostri valori di margine siano in twip

Consapevolezza dei DPI e display ad alti DPI

Su uno schermo che lavora con un ridimensionamento del 150% (144 DPI), la conversione millimetro-pixel restituisce un conteggio di pixel maggiore rispetto a uno schermo al 100%. PDF Library registra le dimensioni in pixel che passate a GetCanvasDC e usa l'argomento DPI in LoadFromCanvasDC per calcolare a ritroso la dimensione fisica della pagina nel PDF. Finché il valore DPI che passate è quello usato per trasformare i millimetri in pixel, la dimensione della pagina di output sarà corretta indipendentemente dal ridimensionamento dello schermo

Se l'applicazione non è compatibile con i DPI (il vecchio predefinito), Windows ridimensiona il DC dello schermo e i calcoli dei pixel saranno errati sulle macchine ad alti DPI. La soluzione più semplice è dichiarare la consapevolezza dei DPI nel manifest dell'applicazione; a quel punto l'applicazione riceve i veri pixel del dispositivo, e il valore Screen.PixelsPerInch che l'esempio passa a LoadFromCanvasDC riporta i DPI reali dello schermo invece di assumere un ridimensionamento al 100%

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 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 costruirlo in una sola sessione: conservate l'ID del documento che la prima chiamata a LoadFromCanvasDC crea, e a ogni passata successiva selezionatelo con SelectDocument e chiamate MergeDocument con l'ID del documento della nuova pagina al posto di SaveToFile, poi salvate una volta sola 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

PDF: scelta tra scrivere un file PDF per passata per un assemblaggio successivo e costruire un unico PDF multipagina accodando pagine dentro il ciclo
Lo stesso ciclo di rendering alimenta o file Output separati o un unico documento in crescita, scambiando flessibilità a valle contro file intermedi

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 mostrati qui fanno parte della losLab PDF Library per Delphi e C++Builder