Technical Article

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

RTF lleva el tiempo suficiente en circulación como para aparecer en lugares que nadie previó: generadores de informes heredados, pipelines de combinación de correspondencia, archivos de documentos legales anteriores a los procesadores de texto modernos. Convertirlo a PDF sobre la marcha es un requisito recurrente, y el enfoque que realmente funciona en Windows no es un analizador RTF dedicado sino la ruta de renderizado que el propio Windows ya proporciona a través de TRichEdit y EM_FORMATRANGE. La edición DLL de la biblioteca PDF 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 para una impresora física. El mensaje EM_FORMATRANGE indica al control que distribuya un rango de caracteres en un DC determinado y devuelve la posición del último carácter que logró ajustar. Al llamarlo repetidamente, avanzando cpMin cada vez, se obtiene la salida página a página. GetCanvasDC de la biblioteca PDF losLab proporciona un DC en memoria dimensionado según las medidas de página que se especifiquen; tras renderizar una página en él, LoadFromCanvasDc captura el resultado como página PDF. Eso es todo el pipeline.

Algo que conviene tener claro desde el principio: el control TRichEdit debe dimensionarse para que coincida con la página de destino. Si el control es más pequeño o más grande que las dimensiones del DC, la paginación no coincidirá con lo que aparezca en el PDF. Para una salida A4 el enfoque habitual es establecer las dimensiones en píxeles del control para que correspondan a 210 x 297 mm a 96 DPI antes de cargar el fichero RTF, utilizando los mismos ayudantes de escala que se usarán para dimensionar el DC.

Implementación en Delphi

Lo que sigue utiliza la unidad de importación PDFlibAX_TLB, que envuelve la edición DLL 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: 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.

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 fichero PDF llamado Output1.pdf, Output2.pdf, etc. Si se desea un único documento multipágina, la API de adición de páginas de la biblioteca permite ensamblarlos a posteriori, o bien se puede reestructurar el bucle para llamar a AddPage dentro de una única sesión de documento. El patrón SaveToFile seguido de RemovePdfDocument por iteración que se muestra arriba mantiene el pico de memoria acotado al contenido de una sola página, lo que importa para ficheros RTF muy extensos.

Detalles de dimensionado que suelen causar problemas

El argumento de 96 DPI en LoadFromCanvasDc indica a la biblioteca con qué resolución de pantalla se renderizó el DC, para que pueda calcular el mapeado correcto de puntos a píxeles para la página PDF. Si este valor es incorrecto, el texto aparecerá con el tamaño equivocado en el resultado aunque la imagen parezca correcta en pantalla.

El +100 añadido a RcPage.Right y RcPage.Bottom es un pequeño margen más allá del borde visible del control. Rich Edit usa el rectángulo rcPage para decidir dónde dividir las páginas; sin el margen, una línea que caiga exactamente en el límite puede duplicarse en dos páginas. No es una constante mágica: ha de ser lo bastante grande para que el límite de página caiga limpiamente dentro del área de composición del control y no en el último píxel.

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 texto enriquecido funcionan sin tratamiento adicional porque Rich Edit las renderiza de forma nativa. El área que puede resultar sorprendente es el texto que usa espaciado de párrafo personalizado o sangrías de primera línea expresadas en twips: el sistema de coordenadas interno de Rich Edit está en twips (1/1440 de pulgada), mientras que las coordenadas del DC que se establecen en TFormatRange están en píxeles al DPI actual. El control convierte internamente, pero si se construye el RTF mediante programación hay que verificar que los valores de margen estén en la unidad correcta.

Compatibilidad con DPI alto

En una pantalla con escala del 150% (144 DPI), ScaleX(210, mmPixel) devolverá un número de píxeles mayor que en una pantalla al 100%. La biblioteca PDF registra las dimensiones en píxeles que se pasan a GetCanvasDC y usa el argumento DPI en LoadFromCanvasDc para calcular el tamaño físico de la página en el PDF. Mientras el valor DPI pasado coincida con el DPI al que se ejecuta la aplicación, el tamaño de la página de salida será correcto independientemente de la escala del monitor.

Si la aplicación no es compatible con DPI alto (el comportamiento antiguo por defecto), Windows escala el DC de pantalla y los cálculos de píxeles serán incorrectos en equipos de alta resolución. La solución más sencilla es declarar la compatibilidad con DPI en el manifiesto de la aplicación; la aplicación recibe entonces píxeles de dispositivo reales y el 96 que se pasa a LoadFromCanvasDc debe reemplazarse por el DPI real del monitor obtenido con GetDeviceCaps(GetDC(0), LOGPIXELSX). El ejemplo de código tiene 96 codificado de forma fija porque es adecuado para un entorno con escala del 100% y mantiene el ejemplo breve.

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

El bucle anterior escribe cada página en un fichero PDF separado. Si eso es lo deseable depende del uso posterior. Los sistemas de generación de informes suelen necesitar páginas individuales porque ensamblan el documento final más adelante mediante fusión o reordenación. Si se desea un único PDF desde el principio, la biblioteca permite crear un documento con varias páginas en una sola sesión: crear el documento una vez fuera del bucle, llamar al método de adición de página en lugar de SaveToFile dentro del bucle y guardar el documento completo cuando el bucle termine. Esto evita los ficheros intermedios y es la estructura correcta para la mayoría de los escenarios de conversión a documento único.

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 losLab PDF Library para Delphi y C++Builder.