Artículo técnico

Conversión de RTF a PDF en Delphi con losLab PDF Library

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

PDF: pipeline de RTF a PDF: un control Rich Edit dimensionado pagina su texto hacia un DC de canvas virtual vía EM_FORMATRANGE y cada pasada se captura como una página PDF
EM_FORMATRANGE acomoda un rango de texto RTF en el DC de canvas virtual y LoadFromCanvasDC captura el resultado, repitiendo hasta llegar al último carácter

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

PDF: rectángulos rcPage y rc anidados para EM_FORMATRANGE, ambos dados en twips convertidos de pixels del canvas al DPI que dimensionó el DC virtual
rcPage es la página completa y rc el área que el texto puede usar; ambos van a EM_FORMATRANGE en twips, mientras el DC de abajo sigue en pixels

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

PDF: Elegir entre escribir un archivo PDF por pasada para ensamblar después y construir un único PDF multipágina agregando páginas dentro del loop
El mismo loop de renderizado alimenta o bien archivos Output separados o un documento que crece, intercambiando flexibilidad río abajo por archivos intermedios

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