Il formato RTF esiste da così tanto tempo che compare in contesti del tutto imprevisti: generatori di report legacy, pipeline di stampa unione, archivi di documenti legali antecedenti ai moderni elaboratori di testi. La conversione di RTF in PDF al volo è un requisito ricorrente, e l'approccio che funziona davvero su Windows non è un parser RTF dedicato, bensì il percorso di rendering che lo stesso Windows già fornisce tramite TRichEdit ed EM_FORMATRANGE. La versione DLL di losLab PDF Library espone un contesto dispositivo (DC) virtuale che si inserisce direttamente in questa pipeline.
Il meccanismo: DC virtuale ed EM_FORMATRANGE
I controlli Rich Edit possono impaginare il loro contenuto per qualsiasi contesto dispositivo, non solo per una stampante fisica. Il messaggio EM_FORMATRANGE indica al controllo di disporre un intervallo di caratteri in un determinato DC e restituisce la posizione dell'ultimo carattere che è riuscito a inserire. Chiamandolo ripetutamente, avanzando ogni volta cpMin, si ottiene un output pagina per pagina. La funzione GetCanvasDC di losLab PDF Library fornisce un DC in memoria ridimensionato in base alle dimensioni di pagina specificate; dopo aver eseguito il rendering di una pagina al suo interno, LoadFromCanvasDc cattura il risultato come una pagina PDF. Questa è l'intera pipeline.
Una cosa da tenere presente fin da subito: il controllo TRichEdit deve essere ridimensionato in modo da corrispondere alla pagina di destinazione. Se il controllo è più piccolo o più grande delle dimensioni del DC, l'impaginazione non si allineerà con il risultato finale nel PDF. Per l'output in formato A4, l'approccio standard consiste nell'impostare le dimensioni in pixel del controllo per corrispondere a 210 x 297 mm a 96 DPI prima di caricare il file RTF, utilizzando gli stessi helper di ridimensionamento che verranno usati per dimensionare il DC.
Implementazione in Delphi
Il codice seguente utilizza l'unità di importazione PDFlibAX_TLB, che incapsula la versione DLL della libreria. Il form ospita un controllo TRichEdit e un pulsante; il gestore dell'evento OnCreate del form ridimensiona il controllo e carica il file RTF, mentre il clic sul pulsante avvia 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);
// 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.
Cosa fa il ciclo
La funzione PrintRtfBox compila la struttura TFormatRange e la passa al controllo Rich Edit tramite SendMessage. Il controllo esegue il rendering dei caratteri a partire da cpMin, fermandosi quando il DC è pieno, e restituisce la posizione del primo carattere che non è stato possibile inserire. Quando il valore restituito è uguale o superiore alla lunghezza totale del testo, tutti i caratteri sono stati renderizzati e la funzione restituisce zero, il che interrompe il ciclo repeat...until.
Ogni iterazione produce un file PDF denominato Output1.pdf, Output2.pdf e così via. Se si desidera invece un unico documento multipagina, l'API di aggiunta delle pagine della libreria consente di unirli successivamente, oppure è possibile ristrutturare il ciclo per chiamare AddPage all'interno di una singola sessione del documento. Il modello che prevede SaveToFile seguito da RemovePdfDocument per ciascuna iterazione mantiene il picco di memoria limitato al contenuto di una sola pagina, il che è fondamentale per file RTF molto lunghi.
Dettagli di ridimensionamento che possono causare errori
L'argomento 96 DPI per LoadFromCanvasDc comunica alla libreria la risoluzione dello schermo a cui è stato renderizzato il DC, in modo da poter calcolare la corretta mappatura da punti a pixel per la pagina PDF. Se questo valore è errato, il testo apparirà con una dimensione sbagliata nell'output, anche se l'immagine sullo schermo sembra corretta.
Il valore +100 aggiunto a RcPage.Right e RcPage.Bottom rappresenta un piccolo margine oltre il bordo visibile del controllo. Rich Edit utilizza il rettangolo rcPage per decidere dove dividere le pagine; senza questo margine, una riga che cade esattamente sul confine potrebbe essere duplicata su due pagine. Non si tratta di una costante magica: si vuole che sia sufficientemente grande in modo che il confine della pagina cada nettamente all'interno dell'area di layout del controllo anziché sull'ultimo pixel.
Infine, il controllo deve essere già associato a una finestra del form visibile durante l'esecuzione di FormCreate, in modo che il suo handle di finestra sia valido prima della prima chiamata a SendMessage. Un controllo TRichEdit creato dinamicamente a runtime richiede una chiamata esplicita a HandleNeeded prima dell'inizio del ciclo di rendering, qualora il form non sia ancora stato 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 utilizzate per la visualizzazione e la stampa. I font descritti nel file RTF che sono installati sulla macchina verranno renderizzati fedelmente; i font mancanti verranno sostituiti in modo silenzioso, il che può alterare la lunghezza delle righe e l'impaginazione. Per le conversioni batch in produzione, vale la pena testarlo esplicitamente: caricare un documento con ogni tipo di carattere utilizzato dalle sorgenti RTF e verificare che il conteggio delle pagine prodotte corrisponda a quello previsto da un'anteprima di stampa manuale.
Le tabelle, le immagini incorporate e la maggior parte delle funzionalità di formattazione Rich Text funzionano senza alcuna gestione aggiuntiva poiché Rich Edit le esegue nativamente. Un'area che può sorprendere è il testo che utilizza una spaziatura personalizzata dei paragrafi o rientri della prima riga espressi in twip: il sistema di coordinate interno di Rich Edit è in twip (1/1440 di pollice), mentre le coordinate DC impostate in TFormatRange sono in pixel al DPI corrente. Il controllo esegue la conversione interna, ma se si sta costruendo l'RTF a livello di codice, è opportuno verificare che i valori dei margini siano nell'unità di misura corretta.
Consapevolezza DPI e display ad alto DPI
Su uno schermo con ridimensionamento al 150% (144 DPI), ScaleX(210, mmPixel) restituirà un numero di pixel maggiore rispetto a uno schermo al 100%. La PDF Library registra le dimensioni in pixel passate a GetCanvasDC e utilizza l'argomento DPI in LoadFromCanvasDc per ricalcolare la dimensione fisica della pagina nel PDF. Finché il valore DPI passato corrisponde a quello 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 comportamento predefinito), Windows ridimensiona il DC dello schermo e i calcoli dei pixel risulteranno errati sulle macchine ad alto DPI. La soluzione più semplice consiste nel dichiarare la compatibilità DPI nel manifest dell'applicazione; l'applicazione riceverà così i pixel effettivi del dispositivo e il valore 96 passato a LoadFromCanvasDc dovrà essere sostituito con il DPI effettivo dello schermo ottenuto tramite GetDeviceCaps(GetDC(0), LOGPIXELSX). L'esempio di codice sopra codifica rigidamente 96 perché è appropriato per un ambiente di ridimensionamento al 100% e mantiene l'esempio breve.
Struttura di output: un file per pagina rispetto a un documento combinato
Il ciclo precedente scrive ogni pagina in un file PDF separato. Se questo sia l'effetto desiderato dipende dall'uso a valle. I sistemi di generazione di report hanno spesso bisogno di singole pagine perché assemblano il documento finale in un secondo momento unendo o riordinando le pagine. Se si desidera un singolo PDF fin dall'inizio, la libreria consente di creare un documento con più pagine in una sola sessione: si crea il documento una volta al di fuori del ciclo, si chiama il metodo di aggiunta della pagina invece di SaveToFile all'interno del ciclo e si salva il documento completo al termine del ciclo. Questo evita la creazione di file intermedi ed è la struttura corretta per la maggior parte degli scenari di conversione di un singolo documento.
Per file RTF di grandi dimensioni, vale la pena aggiungere un feedback sullo stato di avanzamento nel ciclo, poiché la velocità di conversione è all'incirca proporzionale al numero di pagine e un documento di 200 pagine può richiedere alcuni secondi. La struttura repeat...until è facile da estendere: è possibile monitorare l'offset dei caratteri nell'aggiornamento di una barra di avanzamento dopo ogni iterazione, dividendo LastChar per il numero totale di caratteri ottenuto da RichEdit1.GetTextLen.
I metodi GetCanvasDC e LoadFromCanvasDc mostrati qui fanno parte di losLab PDF Library per Delphi e C++Builder.