Technical Article

Converter RTF para PDF em Delphi com a Biblioteca PDF losLab

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.