El formato RTF ha existido el tiempo suficiente como para aparecer en lugares que nadie había planeado: generadores de informes heredados, tuberías de combinación de correspondencia, archivos de documentos legales que son 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 losLab PDF Library expone un contexto de dispositivo virtual que encaja directamente en esa tubería
El mecanismo: DC virtual y EM_FORMATRANGE
Los controles Rich Edit pueden paginar su contenido para cualquier contexto de dispositivo (DC), no solo para una impresora física. El mensaje EM_FORMATRANGE le dice al control que diseñe un rango de caracteres en un DC dado y devuelve la posición del último carácter que logró encajar. Llámelo repetidamente, avanzando cpMin cada vez, y obtendrá una salida página por página. El método GetCanvasDC de losLab PDF Library proporciona un DC en memoria dimensionado a cualquier tamaño de página que especifique; después de renderizar una página en él, LoadFromCanvasDc captura el resultado como una página PDF. Esa es toda la tubería
Una cosa a hacer bien desde el principio: el control TRichEdit debe estar dimensionado 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 se alineará con lo que termina en el PDF. Para la salida A4, el enfoque estándar es configurar las dimensiones de píxeles del control para que coincidan con 210 x 297 mm a 96 DPI antes de cargar el archivo RTF, utilizando los mismos ayudantes de escala que usará para dimensionar el DC
Implementación en Delphi
Lo siguiente 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 impulsa 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é está haciendo el bucle
PrintRtfBox llena la estructura TFormatRange y se la pasa al control Rich Edit a través de SendMessage. El control renderiza los caracteres comenzando en cpMin, deteniéndose cuando el DC se llena, y devuelve la posición del primer carácter que no encajó. Cuando el valor de retorno iguala o supera la longitud total del texto, cada carácter ha sido renderizado y la función devuelve cero, lo que termina el bucle repeat...until
Cada iteración produce un archivo PDF con el nombre Output1.pdf, Output2.pdf, y así sucesivamente. Si en su lugar desea un solo documento de varias páginas, la API de adición de páginas de la biblioteca le permite ensamblarlas a posteriori, o puede reestructurar el bucle para llamar a AddPage dentro de una sola sesión de documento. El patrón de SaveToFile por iteración seguido de RemovePdfDocument de arriba mantiene la memoria pico limitada al contenido de una página, lo que es importante para archivos RTF muy largos
Detalles de tamaño con los que la gente tropieza
El argumento de 96 DPI para LoadFromCanvasDc le dice a la biblioteca a qué resolución de pantalla se renderizó el DC, para que pueda calcular el mapeo correcto de puntos a píxeles para la página PDF. Si se equivoca en esto, el texto aparecerá con el tamaño incorrecto en la salida, aunque la imagen se vea correcta en la pantalla
El +100 sumado 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 cae exactamente en el límite puede duplicarse en dos páginas. No es una constante mágica: desea que sea lo suficientemente grande para que el límite de la página caiga limpiamente dentro del área de diseño del control en lugar de en el último píxel
Finalmente, el control ya debe estar adjunto a una ventana de formulario visible cuando se ejecuta FormCreate para que su identificador 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
Manejo de fuentes y funciones RTF
Debido a que el renderizado se realiza mediante el motor de Rich Edit de Windows, la sustitución de fuentes sigue las mismas reglas que utiliza para la visualización y la impresión. Las fuentes referenciadas en el archivo RTF que estén instaladas en la máquina se renderizarán fielmente; las fuentes que falten se sustituirán silenciosamente, lo que puede cambiar la longitud de las líneas y la paginación. Para la conversión por lotes en producción, vale la pena probar esto explícitamente: cargue un documento con cada tipo de letra que utilicen sus fuentes RTF y confirme que el conteo de páginas de salida coincida con lo que espera de 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 ningún manejo adicional porque Rich Edit las renderiza de forma nativa. El único área que puede sorprender es el texto que utiliza 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 establece en TFormatRange están en píxeles según los DPI actuales. El control se convierte internamente, pero si está construyendo el RTF programáticamente, debe verificar que los valores de sus márgenes estén en la unidad correcta
Conciencia de DPI y pantallas de alto DPI
En una pantalla que se ejecuta a un escalado del 150% (144 DPI), ScaleX(210, mmPixel) devolverá un conteo de píxeles mayor que en una pantalla al 100%. La PDF Library registra cualquier dimensión de píxeles que pase a GetCanvasDC y usa el argumento DPI en LoadFromCanvasDc para calcular hacia atrás el tamaño de la página física en el PDF. Siempre que el valor de DPI que pase coincida con el DPI al que se ejecuta su aplicación, el tamaño de la página de salida será correcto independientemente del escalado de la pantalla
Si su aplicación no es consciente del DPI (el valor predeterminado anterior), Windows escala el DC de la pantalla y sus cálculos de píxeles serán incorrectos en máquinas de alto DPI. La solución más simple es declarar la conciencia del DPI en el manifiesto de la aplicación; la aplicación luego recibe píxeles de dispositivo verdaderos y el 96 que pasa a LoadFromCanvasDc debe reemplazarse con el DPI de la pantalla real obtenido de GetDeviceCaps(GetDC(0), LOGPIXELSX). El código de ejemplo de arriba codifica de forma fija el 96 porque es apropiado para un entorno de escalado del 100% y mantiene el ejemplo breve
Estructura de salida: un archivo por página frente a un documento combinado
El bucle anterior escribe cada página en un archivo PDF separado. Si eso es lo que desea, 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 mediante la fusión o reordenación de páginas. Si desea un solo PDF desde el principio, la biblioteca le permite crear un documento con varias páginas en una sola sesión: cree el documento una vez fuera del bucle, llame al método para agregar páginas en lugar de SaveToFile dentro del bucle, y guarde el documento completo después de que finalice el bucle. Esto evita los archivos intermedios y es la estructura correcta para la mayoría de los escenarios de conversión de documentos únicos
Para archivos RTF grandes, vale la pena agregar algunos comentarios de progreso en el bucle, ya que la tasa de conversión es más o menos proporcional al conteo de páginas y un documento de 200 páginas puede tardar unos segundos. La estructura repeat...until es fácil de extender: realice un seguimiento del desplazamiento de caracteres en una actualización de la barra de progreso después de cada iteración, dividiendo LastChar 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