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
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
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
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