Artículo técnico

Conversión de RTF a PDF en Delphi con la biblioteca PDF

RTF lleva el tiempo suficiente en circulación como para aparecer en lugares que nadie planificó: generadores de informes antiguos, pipelines de mail merge, archivos de documentos legales que preceden 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 RTF dedicado, sino el camino de renderizado que Windows ya proporciona a través de TRichEdit y EM_FORMATRANGE. La biblioteca PDF de losLab expone un contexto de dispositivo virtual que encaja directamente en ese pipeline

El mecanismo: DC virtual y EM_FORMATRANGE

Los controles Rich Edit pueden paginar su contenido para cualquier contexto de dispositivo, no solo una impresora física. El mensaje EM_FORMATRANGE dice al control que disponga un rango de caracteres en un DC dado y devuelve la posición del último carácter que consiguió encajar. Llámalo repetidamente, avanzando cpMin cada vez, y obtienes salida página a página. El GetCanvasDC de la biblioteca PDF de losLab proporciona un DC en memoria dimensionado a las dimensiones de página que especifiques; tras renderizar una página en él, LoadFromCanvasDC captura el resultado como un nuevo documento PDF de una página. Ese es el pipeline entero

Algo que conviene tener claro desde el principio: el control TRichEdit debe dimensionarse para coincidir con la página destino. Si el control es menor o mayor que las dimensiones del DC, la paginación no cuadrará con lo que acabe dentro del PDF. Para salida A4 el enfoque estándar es fijar las dimensiones en píxeles del control para que coincidan con 210 x 297 mm a los mismos DPI que luego pasarás a LoadFromCanvasDC, antes de cargar el archivo RTF, con la misma conversión de milímetros a píxeles que usas para dimensionar el DC

PDF: pipeline de RTF a PDF: un control Rich Edit dimensionado pagina su texto en un DC de canvas virtual a través de EM_FORMATRANGE y cada pasada se captura como una página PDF
EM_FORMATRANGE dispone un rango de texto RTF en el DC de canvas virtual y LoadFromCanvasDC captura el resultado, repitiendo hasta alcanzar el último carácter

Implementación en Delphi

Lo que sigue usa la unit de importación PDFlibAX_TLB, la importación de la librería de tipos para la edición ActiveX de la biblioteca. El formulario aloja un TRichEdit y un botón; el manejador OnCreate del formulario dimensiona el control y carga el RTF, y el clic del botón dirige el bucle 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;
  // Dimensiona el control a A4 en los DPI de la 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
    // Obtén un DC virtual dimensionado a A4
    Dc := PdfDoc.GetCanvasDC(
      MulDiv(210, Screen.PixelsPerInch * 10, 254),
      MulDiv(297, Screen.PixelsPerInch * 10, 254));
    // Renderiza la siguiente página del contenido RTF en el DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Captura 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;
  // Deja que el control libere los datos de formato 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 están en twips (1/1440 de pulgada), no en píxeles: convierte el
  // tamaño de página con los mismos DPI que dimensionaron 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;  // dibuja en la página entera; reduce 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;  // indica la última página
end;

end.

Qué hace el bucle

PrintRtfBox rellena la estructura TFormatRange y la pasa al control Rich Edit mediante SendMessage. El control renderiza los caracteres a partir de cpMin, deteniéndose cuando el DC se llena, y devuelve la posición del primer carácter que no pudo ajustar. Cuando el valor de retorno iguala o supera la longitud total del texto, todos los caracteres se han renderizado y la función devuelve cero, lo que termina el bucle repeat...until

Cada iteración produce un archivo PDF llamado Output1.pdf, Output2.pdf, y así. Si quieres en cambio un único documento multipágina, AddToFileList más MergeFileList pueden ensamblar los archivos por página a posteriori, o puedes reestructurar el bucle para que cada documento de página nueva se pliegue en el primero con MergeDocument. El patrón de arriba de SaveToFile por iteración seguido de RemoveDocument mantiene la memoria pico acotada al contenido de una página, lo que importa en archivos RTF muy largos

Detalles de dimensionado que suelen causar problemas

El argumento DPI de LoadFromCanvasDC dice a la biblioteca cuántos de tus píxeles hacen una pulgada: la página sale con un ancho de ancho de canvas dividido por DPI por 72 puntos, y lo mismo para el alto. Equivócate en esto y el texto aparecerá con tamaño equivocado en la salida aunque la imagen se vea correcta en pantalla

La unidad de rc y rcPage es el detalle que la mayoría de las muestras Delphi de esta técnica hacen mal. EM_FORMATRANGE lee ambos rectángulos en twips, 1/1440 de pulgada, aunque el DC en el que dibuja trabaje en píxeles. Rellénalos con valores en píxeles y el control dispone el texto en una página quince veces demasiado pequeña a 96 DPI: un puñado de palabras por pasada, y un bucle que convierte un documento de diez páginas en cientos de PDFs casi vacíos. Convertir con MulDiv(Pixels, 1440, Dpi), usando los DPI que dimensionaron el DC, pone el salto de página donde el canvas acaba de verdad. rcPage es la página completa; rc es el área que el texto puede ocupar, así que los márgenes salen de reducir rc dentro de ella, también en twips. Cuando el bucle termina, un EM_FORMATRANGE más con un lParam nil dice al control que libere los datos de formato que cacheó para el DC

PDF: rectángulos rcPage y rc anidados para EM_FORMATRANGE, ambos dados en twips convertidos de píxeles de canvas a los DPI que dimensionaron el DC virtual
rcPage es la página entera y rc el área que el texto puede usar; ambos van a EM_FORMATRANGE en twips, mientras el DC de debajo sigue en píxeles

Por último, el control debe estar ya vinculado a una ventana de formulario visible cuando se ejecuta FormCreate, de modo que su manejador de ventana sea válido antes de la primera llamada a SendMessage. Un TRichEdit creado dinámicamente en tiempo de ejecución necesita una llamada explícita a HandleNeeded antes de que comience el bucle de renderizado si el formulario aún no se ha mostrado

Fuentes y características RTF

Dado que el renderizado lo realiza el motor Rich Edit de Windows, la sustitución de fuentes sigue las mismas reglas que usa para la visualización y la impresión. Las fuentes referenciadas en el fichero RTF que estén instaladas en el equipo se renderizarán fielmente; las que falten se sustituirán de forma silenciosa, lo que puede alterar las longitudes de línea y la paginación. Para la conversión en lote en producción merece la pena probarlo explícitamente: cargar un documento con cada tipografía que usen las fuentes RTF y confirmar que el número de páginas del resultado coincide con el esperado en una vista previa de impresión manual

Las tablas, las imágenes incrustadas y la mayoría de las características de formato de Rich Text funcionan sin manejo extra porque Rich Edit las renderiza de forma nativa. La única zona que puede sorprender es el texto que usa espaciado de párrafo personalizado 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 debajo trabaja en píxeles a los DPI actuales. El control mapea una en otra al dibujar, pero si construyes el RTF por programa deberías verificar que tus valores de margen están también en twips

Compatibilidad con DPI alto

En una pantalla con escala del 150% (144 DPI), la conversión de milímetros a píxeles devuelve un conteo de píxeles mayor que en una pantalla al 100%. La PDF Library registra las dimensiones en píxeles que pases 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 DPI que pases sea el que usaste para convertir milímetros en píxeles, el tamaño de página de salida será correcto sin importar la escala de la pantalla

Si tu aplicación no es consciente de DPI (el comportamiento antiguo por defecto), Windows escala el DC de pantalla y tus cálculos en píxeles estarán mal en máquinas de DPI alto. El arreglo más sencillo es declarar conciencia de DPI en el manifiesto de la aplicación; la aplicación recibe entonces píxeles de dispositivo verdaderos, y el valor Screen.PixelsPerInch que la muestra pasa a LoadFromCanvasDC reporta el DPI real de la pantalla en lugar de asumir escala del 100%

Estructura de salida: un fichero por página o un documento único

El bucle de arriba escribe cada página en un archivo PDF separado. Si eso es lo que quieres depende del uso posterior. Los sistemas de generación de informes a menudo necesitan páginas individuales porque ensamblan el documento final más tarde mezclando o reordenando páginas. Si quieres un único PDF desde el principio, la biblioteca te deja construirlo en una sola sesión: conserva el ID del documento que crea la primera llamada a LoadFromCanvasDC, y en cada pasada posterior selecciónalo con SelectDocument y llama a MergeDocument con el ID de documento de la página nueva en lugar de SaveToFile, y guarda una vez cuando el bucle termine. 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 ensamblarlo después o construir un único PDF multipágina añadiendo páginas dentro del bucle
El mismo bucle de renderizado alimenta o bien archivos Output separados o bien un documento que crece, intercambiando flexibilidad posterior por archivos intermedios

Para ficheros RTF grandes merece la pena añadir algún indicador de progreso en el bucle, ya que la velocidad de conversión es aproximadamente proporcional al número de páginas y un documento de 200 páginas puede tardar unos segundos. La estructura repeat...until es fácil de ampliar: hay que seguir el desplazamiento de caracteres en una actualización de barra de progreso tras cada iteración, usando LastChar dividido por el total de caracteres obtenido con RichEdit1.GetTextLen

Los métodos GetCanvasDC y LoadFromCanvasDC mostrados aquí forman parte de la biblioteca PDF de losLab para Delphi y C++Builder