Artigo Técnico

Conversão de RTF para PDF no Delphi com a Biblioteca PDF losLab

O RTF existe há tempo suficiente para aparecer em lugares não planejados: geradores de relatórios legados, pipelines de mala direta, arquivos de documentos legais 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 analisador RTF dedicado, mas o caminho de renderização que o próprio Windows já fornece através de TRichEdit e EM_FORMATRANGE. A edição DLL da Biblioteca PDF losLab expõe um contexto de dispositivo virtual que se encaixa diretamente nesse pipeline

O mecanismo: DC virtual e EM_FORMATRANGE

Os controles Rich Edit podem paginar seu conteúdo para qualquer contexto de dispositivo, não apenas uma impressora física. A mensagem EM_FORMATRANGE informa ao controle para dispor um intervalo de caracteres em um determinado DC e retorna a posição do último caractere que conseguiu encaixar. Chame-a repetidamente, avançando cpMin a cada vez, e você obterá uma saída página por página. O GetCanvasDC da Biblioteca PDF losLab fornece um DC em memória dimensionado para quaisquer dimensões de página que você especificar; após renderizar uma página nele, o LoadFromCanvasDc captura o resultado como uma página PDF. Esse é o pipeline completo

Uma coisa a se acertar logo de cara: o controle TRichEdit deve ser dimensionado para corresponder à página de destino. Se o controle for menor ou maior que as dimensões do DC, a paginação não se alinhará com o que acaba no PDF. Para saída A4, a abordagem padrão é definir as dimensões de pixel do controle para corresponder a 210 x 297 mm em 96 DPI antes de carregar o arquivo RTF, usando os mesmos auxiliares de escala que você usará para dimensionar o DC

Implementação em Delphi

O exemplo a seguir usa a unit de importação PDFlibAX_TLB, que encapsula a edição DLL da biblioteca. O formulário hospeda um TRichEdit e um botão; o manipulador OnCreate do formulário dimensiona o controle e carrega o RTF, e o clique no botão aciona o loop 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 loop está fazendo

O PrintRtfBox preenche a estrutura TFormatRange e a passa para o controle Rich Edit por meio de SendMessage. O controle renderiza os caracteres começando em cpMin, parando quando o DC fica cheio, e retorna a posição do primeiro caractere que não coube. Quando o valor de retorno é igual ou excede o comprimento total do texto, cada caractere foi renderizado e a função retorna zero, o que encerra o loop repeat...until

Cada iteração produz um arquivo PDF chamado Output1.pdf, Output2.pdf e assim por diante. Se você quiser um único documento com várias páginas, a API de acréscimo de página da biblioteca permite montá-los posteriormente, ou você pode reestruturar o loop para chamar AddPage dentro de uma sessão de documento único. O padrão de chamada por iteração de SaveToFile seguido por RemovePdfDocument acima mantém o pico de memória limitado ao conteúdo equivalente a uma página, o que é importante para arquivos RTF muito longos

Detalhes de dimensionamento que atrapalham

O argumento de 96 DPI para LoadFromCanvasDc informa à biblioteca em qual resolução de tela o DC foi renderizado, para que ela possa calcular o mapeamento correto de pontos para pixels para a página PDF. Entenda isso de forma errada e o texto aparecerá com o tamanho incorreto na saída, mesmo que a imagem pareça correta na tela

O +100 adicionado a RcPage.Right e RcPage.Bottom é uma pequena margem além da borda visível do controle. O Rich Edit usa o retângulo rcPage para decidir onde dividir as páginas; sem a margem, uma linha que cai exatamente no limite pode ser duplicada em duas páginas. Não é uma constante mágica: você quer que seja grande o suficiente para que o limite da página caia de forma limpa dentro da área de layout do controle em vez do último pixel

Por fim, o controle já deve estar anexado a uma janela de formulário visível quando o FormCreate for executado, para que seu handle de janela seja válido antes da primeira chamada para SendMessage. Um TRichEdit criado dinamicamente em tempo de execução precisa de uma chamada explícita para HandleNeeded antes que o loop de renderização comece se o formulário ainda não tiver sido exibido

Lidando com fontes e recursos RTF

Como a renderização é feita pelo mecanismo do Windows Rich Edit, a substituição de fontes segue as mesmas regras que usa para exibição e impressão. As fontes referenciadas no arquivo RTF que estão instaladas na máquina serão renderizadas fielmente; as fontes ausentes serão substituídas silenciosamente, o que pode alterar os comprimentos das linhas e a paginação. Para conversão em lote em produção, vale a pena testar isso explicitamente: carregue um documento com cada tipo de fonte que suas fontes RTF usam e confirme se a contagem de páginas de saída corresponde ao que você espera de uma visualização de impressão manual

Tabelas, imagens incorporadas e a maioria dos recursos de formatação Rich Text funcionam sem qualquer tratamento extra porque o Rich Edit os renderiza nativamente. A única área que pode ser surpreendente é o texto que usa espaçamento de parágrafo personalizado ou recuos de primeira linha expressos em twips: o sistema de coordenadas interno do Rich Edit é em twips (1/1440 de polegada), enquanto as coordenadas DC que você define em TFormatRange estão em pixels no DPI atual. O controle converte internamente, mas se você estiver construindo o RTF programaticamente, verifique se os valores da sua margem estão na unidade correta

Reconhecimento de DPI e telas de alto DPI

Em um monitor rodando com escala de 150% (144 DPI), ScaleX(210, mmPixel) retornará uma contagem de pixels maior do que em um monitor de 100%. A Biblioteca PDF registra quaisquer dimensões de pixel que você passar para GetCanvasDC e usa o argumento DPI em LoadFromCanvasDc para calcular retroativamente o tamanho da página física no PDF. Contanto que o valor DPI que você passe corresponda ao DPI em que seu aplicativo está rodando, o tamanho da página de saída estará correto, independentemente do dimensionamento da tela

Se o seu aplicativo não for compatível com DPI (o padrão antigo), o Windows dimensionará o DC da tela e os seus cálculos de pixel estarão incorretos em máquinas de alto DPI. A correção mais simples é declarar o reconhecimento de DPI no manifesto do aplicativo; o aplicativo, então, recebe pixels de dispositivo verdadeiros e o 96 que você passa para LoadFromCanvasDc deve ser substituído pelo DPI de tela real obtido de GetDeviceCaps(GetDC(0), LOGPIXELSX). O exemplo de código acima define 96 porque é apropriado para um ambiente com escala de 100% e mantém o exemplo curto

Estrutura de saída: um arquivo por página versus um documento combinado

O loop acima grava cada página em um arquivo PDF separado. Se isso é o que você deseja, depende do uso posterior. Os sistemas de geração de relatórios geralmente precisam de páginas individuais porque eles montam o documento final mais tarde, combinando ou reordenando as páginas. Se você deseja um único PDF desde o início, a biblioteca permite criar um documento com várias páginas em uma única sessão: crie o documento uma vez fora do loop, chame o método de adição de página em vez de SaveToFile dentro do loop e salve o documento completo depois que o loop terminar. Isso evita os arquivos intermediários e é a estrutura correta para a maioria dos cenários de conversão de documento único

Para arquivos RTF grandes, vale a pena adicionar algum feedback de progresso no loop, pois a taxa de conversão é quase proporcional à contagem de páginas e um documento de 200 páginas pode levar alguns segundos. A estrutura repeat...until é fácil de estender: rastreie o deslocamento de caracteres na atualização de uma barra de progresso após cada iteração, usando LastChar dividido pela contagem total de caracteres de RichEdit1.GetTextLen

Os métodos GetCanvasDC e LoadFromCanvasDc mostrados aqui fazem parte da Biblioteca PDF losLab para Delphi e C++Builder