RTF lleva suficiente tiempo entre nosotros que aparece en lugares donde nadie lo planeó: generadores de reportes legados, pipelines de mail merge, archivos de documentos legales que anteceden a los procesadores de texto modernos. Convertirlo a PDF al vuelo es un requisito recurrente, y el enfoque que de verdad funciona en Windows no es un parser de RTF dedicado sino el camino de renderizado que Windows mismo ya provee vía TRichEdit y EM_FORMATRANGE. La losLab PDF Library expone un device context virtual que se enchufa directo en ese pipeline
El mecanismo: DC virtual y EM_FORMATRANGE
Los controles Rich Edit pueden paginar su contenido para cualquier device context, no solo una impresora física. El mensaje EM_FORMATRANGE le dice al control que acomode un rango de caracteres en un DC dado y devuelve la posición del último carácter que alcanzó a meter. Llámelo repetidamente, avanzando cpMin cada vez, y obtiene salida página por página. El GetCanvasDC de la losLab PDF Library provee un DC en memoria dimensionado a las dimensiones de página que usted especifique; tras renderizar una página en él, LoadFromCanvasDC captura el resultado como un nuevo documento PDF de una página. Ese es todo el pipeline
Una cosa que hay que resolver de entrada: el control TRichEdit debe dimensionarse para coincidir con la página destino. Si el control es más chico o más grande que las dimensiones del DC, la paginación no va a coincidir con lo que termine en el PDF. Para salida A4 el enfoque estándar es fijar las dimensiones en pixels del control para que coincidan con 210 x 297 mm al mismo DPI que luego pasará a LoadFromCanvasDC, antes de cargar el archivo RTF, con la misma conversión de milímetros a pixels que usa para dimensionar el DC
Implementación en Delphi
Lo siguiente usa la unidad de import PDFlibAX_TLB, la import de type library para la edición ActiveX de la librería. El formulario hospeda un TRichEdit y un botón; el handler de OnCreate del formulario dimensiona el control y carga el RTF, y el clic del botón maneja el loop de conversión
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;
// Dimensione el control a A4 en el DPI de pantalla para que la paginación coincida con el 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
// Obtenga un DC virtual dimensionado a A4
Dc := PdfDoc.GetCanvasDC(
MulDiv(210, Screen.PixelsPerInch * 10, 254),
MulDiv(297, Screen.PixelsPerInch * 10, 254));
// Renderice la próxima página de contenido RTF en el DC
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// Capture el contenido del DC como un 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;
// Deje que el control suelte los datos de formateo que cacheó para el 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 y rcPage van en twips (1/1440 de pulgada), no en pixels: convierta el
// tamaño de página con el mismo DPI que dimensionó el DC y el control
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; // dibuje en la página completa; encoja para márgenes
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; // señala la última página
end;
end.
Qué está haciendo el loop
PrintRtfBox llena la estructura TFormatRange y se la pasa al control Rich Edit vía SendMessage. El control renderiza caracteres empezando en cpMin, se detiene cuando el DC se llena, y devuelve la posición del primer carácter que no cupo. Cuando el valor de retorno iguala o supera la longitud total del texto, todos los caracteres se renderizaron y la función devuelve cero, lo que termina el loop repeat...until
Cada iteración produce un archivo PDF llamado Output1.pdf, Output2.pdf, y así. Si quiere un único documento multipágina en cambio, AddToFileList más MergeFileList pueden ensamblar los archivos por página después de los hechos, o puede reestructurar el loop para que cada documento de página nueva se pliegue en el primero con MergeDocument. El patrón de SaveToFile seguido de RemoveDocument por iteración de arriba mantiene la memoria pico acotada al contenido de una página, lo que importa para archivos RTF muy largos
Detalles de dimensionado que hacen tropezar a la gente
El argumento DPI de LoadFromCanvasDC le dice a la librería cuántos de sus pixels hacen una pulgada: la página sale con un ancho en puntos de (ancho del canvas / DPI) × 72, y lo mismo para el alto. Equivoque esto y el texto aparecerá con el tamaño equivocado en la salida aunque la imagen se vea correcta en pantalla
La unidad de rc y rcPage es el detalle que más muestras Delphi de esta técnica equivocan. EM_FORMATRANGE lee ambos rectángulos en twips, 1/1440 de pulgada, aunque el DC en el que dibuja trabaje en pixels. Llámelos con valores en pixels y el control acomoda el texto en una página quince veces demasiado chica a 96 DPI: un puñado de palabras por pasada, y un loop que convierte un documento de diez páginas en cientos de PDFs casi vacíos. Convertir con MulDiv(Pixels, 1440, Dpi), usando el DPI que dimensionó el DC, pone el salto de página donde el canvas de verdad termina. rcPage es la página completa; rc es el área que el texto puede ocupar, así que los márgenes salen de encoger rc dentro de ella, también en twips. Cuando el loop termina, un EM_FORMATRANGE más con un lParam nil le dice al control que libere los datos de formateo que cacheó para el DC
Por último, el control debe estar ya acoplado a una ventana de formulario visible cuando corra FormCreate, de modo que su window handle sea válido antes de la primera llamada a SendMessage. Un TRichEdit creado dinámicamente en runtime necesita una llamada explícita a HandleNeeded antes de que arranque el loop de renderizado si el formulario aún no se ha mostrado
Manejo de fuentes y características RTF
Como el renderizado lo hace el motor Rich Edit de Windows, la sustitución de fuentes sigue las mismas reglas que usa para display e impresión. Las fuentes referenciadas en el archivo RTF que estén instaladas en la máquina renderizan fielmente; las que faltan se sustituyen en silencio, lo que puede desplazar longitudes de línea y paginación. Para conversión por lotes en producción esto vale la pena probarlo explícitamente: cargue un documento con cada typeface que usen sus fuentes RTF y confirme que el conteo de páginas de la salida coincide con lo que espera de una vista previa de impresión manual
Tablas, imágenes embebidas y la mayoría de las características de formato Rich Text funcionan sin manejo extra porque Rich Edit las renderiza de forma nativa. El área que puede sorprender es el texto que usa espaciado de párrafo custom o sangrías de primera línea expresadas en twips: Rich Edit mide en twips (1/1440 de pulgada), la misma unidad que los rectángulos de TFormatRange, mientras el DC de abajo trabaja en pixels al DPI actual. El control mapea una sobre la otra al dibujar, pero si usted está construyendo el RTF por programación debería verificar que sus valores de margen estén también en twips
DPI awareness y displays de DPI alto
En un display corriendo a escalado del 150% (144 DPI), la conversión de milímetros a pixels devuelve un conteo de pixels mayor que en un display al 100%. La PDF Library registra las dimensiones en pixels que usted pase a GetCanvasDC y usa el argumento DPI de LoadFromCanvasDC para recalcular hacia atrás el tamaño físico de página en el PDF. Mientras el valor de DPI que pase sea el que usó para convertir milímetros a pixels, el tamaño de página de la salida será correcto sin importar el escalado del display
Si su aplicación es DPI-unaware (el default viejo), Windows escala el DC de pantalla y sus cálculos en pixels estarán mal en máquinas de DPI alto. El arreglo más simple es declarar DPI awareness en el manifest de la aplicación; la aplicación entonces recibe pixels de device de verdad, y el valor de Screen.PixelsPerInch que la muestra pasa a LoadFromCanvasDC reporta el DPI real del display en vez de asumir escalado del 100%
Estructura de salida: un archivo por página versus un documento combinado
El loop de arriba escribe cada página a un archivo PDF separado. Si eso es lo que usted quiere depende del uso río abajo. Los sistemas de generación de reportes a menudo necesitan páginas individuales porque arman el documento final después mezclando o reordenando páginas. Si quiere un único PDF desde el principio, la librería le permite construirlo en una sesión: guarde el ID del documento que crea la primera llamada a LoadFromCanvasDC, y en cada pasada posterior selecciónelo con SelectDocument y llame a MergeDocument con el ID del documento de la página nueva en vez de SaveToFile, luego guarde una vez después de que el loop salga. Esto evita los archivos intermedios y es la estructura correcta para la mayoría de los escenarios de conversión de documento único
Para archivos RTF grandes vale la pena agregar feedback de progreso en el loop, ya que la tasa de conversión es aproximadamente proporcional al conteo de páginas y un documento de 200 páginas puede tomar unos segundos. La estructura repeat...until es fácil de extender: lleve el offset de caracteres en una actualización de barra de progreso después de cada iteración, usando LastChar dividido por el conteo total de caracteres de RichEdit1.GetTextLen
Los métodos GetCanvasDC y LoadFromCanvasDC mostrados aquí son parte de la losLab PDF Library para Delphi y C++Builder