O RTF existe há tempo suficiente para aparecer em sítios que ninguém planeou: geradores de relatórios legados, pipelines de impressão em série, arquivos de documentos jurídicos anteriores aos processadores de texto modernos. Convertê-lo para PDF em tempo real é um requisito recorrente, e a abordagem que realmente funciona no Windows não é um parser RTF dedicado, mas o caminho de renderização que o próprio Windows já disponibiliza através do TRichEdit e do EM_FORMATRANGE. A edição DLL da Biblioteca PDF losLab expõe um contexto de dispositivo virtual que se integra diretamente nesse pipeline.
O mecanismo: DC virtual e EM_FORMATRANGE
Os controlos Rich Edit podem paginar o seu conteúdo para qualquer contexto de dispositivo, não apenas para uma impressora física. A mensagem EM_FORMATRANGE instrui o controlo a dispor um intervalo de carateres num dado DC e devolve a posição do último caráter que conseguiu encaixar. Chamando-a repetidamente, avançando o cpMin de cada vez, obtém-se saída página a página. O GetCanvasDC da Biblioteca PDF losLab fornece um DC em memória dimensionado de acordo com as dimensões de página que especificar; após renderizar uma página nele, o LoadFromCanvasDc captura o resultado como uma página PDF. Este é o pipeline completo.
Uma coisa a acertar desde o início: o controlo TRichEdit deve ser dimensionado para corresponder à página de destino. Se o controlo for maior ou menor do que as dimensões do DC, a paginação não corresponderá ao que fica no PDF. Para saída em A4, a abordagem padrão é definir as dimensões em píxeis do controlo para corresponder a 210 x 297 mm a 96 DPI antes de carregar o ficheiro RTF, usando os mesmos auxiliares de escala que utilizará para dimensionar o DC.
Implementação em Delphi
O exemplo seguinte usa a unidade de importação PDFlibAX_TLB, que encapsula a edição DLL da biblioteca. O formulário aloja um TRichEdit e um botão; o gestor OnCreate do formulário dimensiona o controlo e carrega o RTF, e o clique no botão conduz o ciclo de conversão.
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.
O que o ciclo está a fazer
O PrintRtfBox preenche a estrutura TFormatRange e passa-a para o controlo Rich Edit via SendMessage. O controlo renderiza os carateres a partir de cpMin, parando quando o DC fica cheio, e devolve a posição do primeiro caráter que não coube. Quando o valor de retorno é igual ou superior ao comprimento total do texto, todos os carateres foram renderizados e a função devolve zero, o que termina o ciclo repeat...until.
Cada iteração produz um ficheiro PDF com o nome Output1.pdf, Output2.pdf, e assim por diante. Se quiser um único documento com várias páginas, a API de adição de páginas da biblioteca permite montá-las depois, ou pode reestruturar o ciclo para chamar AddPage dentro de uma única sessão de documento. O padrão de SaveToFile seguido de RemovePdfDocument em cada iteração mantém o pico de memória limitado ao conteúdo de uma página, o que é importante para ficheiros RTF muito longos.
Detalhes de dimensionamento que confundem as pessoas
O argumento de 96 DPI para o LoadFromCanvasDc indica à biblioteca a que resolução de ecrã o DC foi renderizado, para que possa calcular o mapeamento correto de pontos para píxeis para a página PDF. Se este valor estiver errado, o texto aparecerá com o tamanho errado na saída mesmo que a imagem pareça correta no ecrã.
O +100 adicionado a RcPage.Right e RcPage.Bottom é uma pequena margem para além da borda visível do controlo. O Rich Edit usa o retângulo rcPage para decidir onde dividir as páginas; sem a margem, uma linha que caia exatamente na fronteira pode ser duplicada em duas páginas. Não é uma constante mágica: deve ser suficientemente grande para que o limite da página caia claramente dentro da área de disposição do controlo, e não no último píxel.
Por fim, o controlo já deve estar associado a uma janela de formulário visível quando o FormCreate é executado, para que o seu identificador de janela seja válido antes da primeira chamada ao SendMessage. Um TRichEdit criado dinamicamente em tempo de execução precisa de uma chamada explícita a HandleNeeded antes de o ciclo de renderização começar, se o formulário ainda não tiver sido mostrado.
Tratar tipos de letra e funcionalidades RTF
Como a renderização é feita pelo motor Windows Rich Edit, a substituição de tipos de letra segue as mesmas regras que utiliza para apresentação e impressão. Os tipos de letra referenciados no ficheiro RTF que estejam instalados na máquina serão renderizados fielmente; os tipos de letra em falta serão substituídos silenciosamente, o que pode alterar os comprimentos das linhas e a paginação. Para conversão em lote de produção, vale a pena testar isto explicitamente: carregue um documento com cada família tipográfica que as suas fontes RTF utilizam e confirme que a contagem de páginas da saída corresponde ao que espera de uma pré-visualização de impressão manual.
As tabelas, imagens incorporadas e a maioria das funcionalidades de formatação Rich Text funcionam sem qualquer processamento adicional porque o Rich Edit as renderiza nativamente. A área que pode surpreender é o texto que usa espaçamento de parágrafo personalizado ou avanços de primeira linha expressos em twips: o sistema de coordenadas interno do Rich Edit está em twips (1/1440 de polegada), enquanto as coordenadas do DC que define em TFormatRange estão em píxeis à DPI atual. O controlo converte internamente, mas se estiver a construir o RTF programaticamente, deve verificar que os valores das margens estão na unidade correta.
Compatibilidade com DPI e ecrãs de alta DPI
Num ecrã a funcionar a 150% de escala (144 DPI), o ScaleX(210, mmPixel) devolverá uma contagem de píxeis maior do que num ecrã a 100%. A Biblioteca PDF regista as dimensões em píxeis que passa ao GetCanvasDC e usa o argumento DPI em LoadFromCanvasDc para calcular de volta o tamanho físico da página no PDF. Desde que o valor de DPI que passa corresponda à DPI a que a sua aplicação está a correr, o tamanho da página de saída estará correto independentemente da escala do ecrã.
Se a sua aplicação não tem compatibilidade com DPI (o antigo comportamento predefinido), o Windows escala o DC do ecrã e os seus cálculos de píxeis estarão errados em máquinas de alta DPI. A solução mais simples é declarar a compatibilidade com DPI no manifesto da aplicação; a aplicação recebe então os píxeis de dispositivo reais e o valor 96 que passa ao LoadFromCanvasDc deve ser substituído pela DPI real do ecrã obtida de GetDeviceCaps(GetDC(0), LOGPIXELSX). O exemplo de código acima codifica 96 de forma fixa porque é apropriado para um ambiente a 100% de escala e mantém o exemplo conciso.
Estrutura da saída: um ficheiro por página ou um documento combinado
O ciclo acima escreve cada página num ficheiro PDF separado. Se é isso que pretende depende da utilização posterior. Os sistemas de geração de relatórios precisam frequentemente de páginas individuais porque montam o documento final mais tarde, fundindo ou reordenando páginas. Se quiser um único PDF desde o início, a biblioteca permite criar um documento com várias páginas numa única sessão: crie o documento uma vez fora do ciclo, chame o método de adição de página em vez de SaveToFile dentro do ciclo e guarde o documento completo após o ciclo terminar. Isto evita os ficheiros intermédios e é a estrutura correta para a maioria dos cenários de conversão de documento único.
Para ficheiros RTF grandes, vale a pena adicionar algum feedback de progresso no ciclo, uma vez que a taxa de conversão é aproximadamente proporcional ao número de páginas e um documento de 200 páginas pode demorar alguns segundos. A estrutura repeat...until é fácil de estender: acompanhe o deslocamento do caráter numa atualização de barra de progresso após cada iteração, usando LastChar dividido pela contagem total de carateres de RichEdit1.GetTextLen.
Os métodos GetCanvasDC e LoadFromCanvasDc aqui apresentados fazem parte da Biblioteca PDF losLab para Delphi e C++Builder.