Artigo Técnico

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

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 Biblioteca PDF losLab expõe um contexto de dispositivo virtual que se encaixa direto 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 diz ao controle para dispor um intervalo de caracteres num dado DC e devolve a posição do último caractere que conseguiu encaixar. Chame-a repetidamente, avançando cpMin a cada vez, e você obtém 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; depois de renderizar uma página nele, o LoadFromCanvasDC captura o resultado como um novo documento PDF de uma página. Esse é o pipeline completo

Uma coisa a acertar de cara: o controle TRichEdit precisa 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 vai 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 no mesmo DPI que você depois passará ao LoadFromCanvasDC, antes de carregar o arquivo RTF, com a mesma conversão de milímetro para pixel que você usa para dimensionar o DC

PDF: pipeline de RTF para PDF: um controle Rich Edit dimensionado pagina seu texto em um DC de canvas virtual por meio de EM_FORMATRANGE e cada passada é capturada como uma página de PDF
EM_FORMATRANGE compõe um intervalo de texto RTF no DC de canvas virtual e o LoadFromCanvasDC captura o resultado, repetindo até o último caractere ser alcançado

Implementação em Delphi

O seguinte usa a unit de importação PDFlibAX_TLB, a importação de type library para a edição ActiveX 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: IPDFlibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := CoPDFlibrary.Create;
  // Dimensiona o controle para A4 no DPI da tela para que a paginação corresponda ao 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ém um DC virtual dimensionado para A4
    Dc := PdfDoc.GetCanvasDC(
      MulDiv(210, Screen.PixelsPerInch * 10, 254),
      MulDiv(297, Screen.PixelsPerInch * 10, 254));
    // Renderiza a próxima página do conteúdo RTF no DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Captura o conteúdo do DC como um 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;
  // Deixa o controle soltar os dados de formatação que ele cacheou para o 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 e rcPage estão em twips (1/1440 de polegada), não em pixels: converta o
  // tamanho da página com o mesmo DPI que dimensionou o DC e o controle
  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;  // desenha na página inteira; encolha para margens
  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;  // sinaliza a última página
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ê quer um único documento de várias páginas em vez disso, AddToFileList mais MergeFileList podem montar os arquivos por página depois do fato, ou você pode reestruturar o loop para que cada novo documento de página seja incorporado ao primeiro com MergeDocument. O padrão de SaveToFile por iteração seguido de RemoveDocument acima mantém o pico de memória limitado ao conteúdo de uma página, o que importa para arquivos RTF muito longos

Detalhes de dimensionamento que atrapalham

O argumento de DPI para o LoadFromCanvasDC diz à biblioteca quantos dos seus pixels fazem uma polegada: a página sai com a largura do canvas dividida pelo DPI vezes 72 pontos de largura, e o mesmo para a altura. Entenda isso errado e o texto aparecerá com o tamanho errado na saída mesmo que a imagem pareça correta na tela

A unidade de rc e rcPage é o detalhe que a maioria das amostras Delphi dessa técnica erra. O EM_FORMATRANGE lê os dois retângulos em twips, 1/1440 de polegada, mesmo que o DC no qual ele desenha trabalhe em pixels. Preenchê-los com valores de pixel faz o controle dispor o texto numa página quinze vezes menor a 96 DPI: um punhado de palavras por passada, e um loop que transforma um documento de dez páginas em centenas de PDFs quase vazios. Converter com MulDiv(Pixels, 1440, Dpi), usando o DPI que dimensionou o DC, põe a quebra de página onde o canvas de fato termina. O rcPage é a página inteira; o rc é a área que o texto pode ocupar, então as margens vêm de encolher o rc dentro dele, também em twips. Quando o loop termina, mais um EM_FORMATRANGE com lParam nil diz ao controle para liberar os dados de formatação que ele cacheou para o DC

PDF: Retângulos rcPage e rc aninhados para o EM_FORMATRANGE, ambos dados em twips convertidos dos pixels do canvas no DPI que dimensionou o DC virtual
O rcPage é a página inteira e o rc a área que o texto pode usar; ambos vão ao EM_FORMATRANGE em twips, enquanto o DC por baixo continua em pixels

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 tratamento extra nenhum porque o Rich Edit os renderiza nativamente. A única área que pode surpreender é texto que usa espaçamento de parágrafo personalizado ou recuos de primeira linha expressos em twips: o Rich Edit mede em twips (1/1440 de polegada), a mesma unidade dos retângulos no TFormatRange, enquanto o DC por baixo trabalha em pixels no DPI atual. O controle mapeia um no outro enquanto desenha, mas se você está construindo o RTF programaticamente, verifique se os seus valores de margem estão em twips também

Reconhecimento de DPI e telas de alto DPI

Num monitor rodando com escala de 150% (144 DPI), a conversão de milímetro para pixel devolve uma contagem de pixels maior que num monitor de 100%. A Biblioteca PDF registra quaisquer dimensões de pixel que você passar ao GetCanvasDC e usa o argumento DPI no LoadFromCanvasDC para retrocalcular o tamanho físico da página no PDF. Contanto que o valor de DPI que você passar seja o que você usou para transformar milímetros em pixels, o tamanho da página de saída estará correto independente da escala da tela

Se o seu aplicativo não reconhece DPI (o padrão antigo), o Windows dimensiona o DC da tela e os seus cálculos de pixel estarão errados em máquinas de alto DPI. O conserto mais simples é declarar reconhecimento de DPI no manifesto do aplicativo; o aplicativo então recebe pixels de dispositivo verdadeiros, e o valor Screen.PixelsPerInch que a amostra passa ao LoadFromCanvasDC reporta o DPI real da tela em vez de presumir escala de 100%

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

O loop acima grava cada página num arquivo PDF separado. Se isso é o que você quer depende do uso a jusante. Sistemas de geração de relatórios frequentemente precisam de páginas individuais porque montam o documento final depois, mesclando ou reordenando páginas. Se você quer um único PDF desde o início, a biblioteca deixa construí-lo numa sessão: guarde o ID do documento que a primeira chamada de LoadFromCanvasDC cria, e a cada passada seguinte selecione-o com SelectDocument e chame MergeDocument com o ID do documento da nova página em vez de SaveToFile, então salve uma vez depois de o loop sair. Isso evita os arquivos intermediários e é a estrutura certa para a maioria dos cenários de conversão de documento único

PDF: escolher entre gravar um arquivo PDF por passada para montagem posterior e construir um único PDF multipágina anexando páginas dentro do loop
O mesmo loop de renderização alimenta ou arquivos Output separados ou um único documento em crescimento, trocando flexibilidade a jusante por arquivos intermediários

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