Technical Article

Criar um Visualizador de PDF em Delphi com o PDFium VCL

Um visualizador de PDF em Delphi resume-se a dois componentes e à ligação entre eles. O TPdf é o proprietário do documento: abre o ficheiro, desencripta-o e responde a questões sobre a contagem de páginas e metadados. O TPdfView é o controlo visual que desenha as páginas no ecrã e gere o deslocamento (scrolling), o zoom e a página que o utilizador está a visualizar no momento. O PDFium VCL incorpora o mesmo motor de renderização que é distribuído com o Chrome, pelo que os glifos, o anti-aliasing e as cores obtidos no canvas correspondem ao que os seus utilizadores já veem no navegador. O trabalho não está na renderização. Está em ligar o objeto do documento à visualização, carregar sem falhas em ficheiros corrompidos ou protegidos por palavra-passe, e fornecer ao utilizador os poucos controlos que fazem com que um visualizador pareça terminado: mudar de página, alterar o zoom, ajustar a página à janela.

Este artigo percorre essa montagem na ordem em que a constrói. Tudo aqui renderiza uma página de cada vez, que é o que a maioria dos fluxos de trabalho de documentos exige. Se necessitar de páginas empilhadas numa única coluna com deslocamento contínuo, essa é uma decisão de layout diferente abordada num artigo separado e não é o caminho seguido aqui.

Ligar o TPdf ao TPdfView

Insira um TPdf e um TPdfView no formulário e depois indique à visualização qual o documento a apresentar. Essa única atribuição é toda a ligação necessária entre o documento não visual e o controlo que o desenha.

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

Antes de tudo isto ser executado, a biblioteca nativa do PDFium tem de estar na máquina. O PDFium VCL faz chamadas para a pdfium32.dll ou pdfium64.dll, dependendo da sua plataforma de destino, e o documento simplesmente recusa-se a abrir se a DLL não for encontrada. Distribua a DLL correspondente junto ao seu executável ou coloque-a num local onde o carregador do sistema a encontre. As compilações com suporte a V8 existem apenas para PDFs que contêm JavaScript que pretenda executar, o que um visualizador simples não necessita, pelo que deve optar pela DLL padrão, a menos que tenha um motivo concreto para não o fazer.

Carregar um documento sem confiar na entrada

O instinto é envolver o carregamento num bloco try/except e tratar uma exceção gerada como uma falha. Esse instinto está incorreto aqui, e errar nesse aspeto produz um visualizador que parece bom até que alguém lhe passe um ficheiro corrompido. Definir Active := True não gera exceções em caso de falha de carregamento. O PDFium VCL captura o erro interno e mantém a propriedade Active como False, pelo que a única forma honesta de saber se o documento abriu é ler a propriedade de volta após a definir.

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

Dois aspetos merecem atenção. O primeiro é que o PageNumber existe em ambos os objetos e os dois são independentes. O Pdf.PageNumber é a noção do documento sobre a página atual; o PdfView.PageNumber é a página que o controlo realmente apresenta, e é a que define para mover o utilizador pelo ficheiro. Definir um não move o outro, pelo que um visualizador controla sempre a propriedade da visualização (view). O segundo aspeto é a indexação baseada em 1: as páginas vão de 1 a Pdf.PageCount, e não a partir de 0, o que pode surpreender quem está habituado a arrays baseados em zero.

Gere um ficheiro encriptado

Os documentos encriptados integram-se no mesmo fluxo de carregamento. Se a palavra-passe de abertura for definida antes da ativação, o documento é desencriptado à medida que abre; se estiver incorreta ou em falta, a propriedade Active permanece False, exatamente como acontece com um ficheiro corrompido. Assim, a recuperação consiste em solicitar uma palavra-passe e tentar a ativação novamente.

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Como a falha é silenciosa tanto para uma palavra-passe incorreta como para um ficheiro danificado, não é possível distingui-las apenas pela propriedade Active. Na prática, isto é aceitável para um visualizador: o utilizador fornece a palavra-passe correta ou percebe que o ficheiro não abre, e a mensagem é a mesma em ambos os casos.

Navegar pelas páginas do documento

Com o documento aberto, a navegação é uma operação aritmética no PdfView.PageNumber limitada pelo Pdf.PageCount. O único trabalho real é a limitação dos valores (clamping), para que os botões nunca empurrem a página para fora do intervalo e os botões de primeira e última página fiquem desativados nos limites do ficheiro.

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

Uma caixa de texto \"ir para a página N\" é a mesma chamada GoToPage alimentada por um número inteiro convertido, e a limitação cobre o caso em que o utilizador digita 9999 num ficheiro de dez páginas. Mantenha o UpdatePageLabel como o único local que escreve \"Página 3 de 12\" para que a leitura nunca fique dessincronizada com o que a visualização apresenta.

Zoom: percentagens explícitas e modos de ajuste

O zoom no TPdfView apresenta-se em duas variantes que interagem, e compreender essa interação é a diferença entre um controlo de zoom que se comporta corretamente e um que entra em conflito com o utilizador. O caminho direto é a propriedade Zoom, uma percentagem onde 100 significa o tamanho real. O outro caminho é o FitMode, que indica à visualização para calcular o zoom por si e continuar a recalculá-lo à medida que a janela é redimensionada.

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

Aqui está a parte que confunde as pessoas. Atribuir o Zoom diretamente repõe o FitMode para pfmNone. Este é o comportamento correto, não um bug: no momento em que o utilizador escolhe exatamente 150%, a visualização já não pode honrar o \"ajustar à largura\", porque os dois pedidos entram em conflito. A consequência para a sua interface de utilizador é que um botão de aproximação (zoom-in) e um botão de ajustar à página são estados mutuamente exclusivos, e a barra de ferramentas deve tornar visível o modo ativo. Quando o utilizador clica em ajustar à página, defina o FitMode; quando clica num zoom numérico, defina o Zoom e deixe que este limpe o modo de ajuste por si só.

Se preferir calcular o valor de ajuste por si, talvez para alimentar um controlo de deslize de zoom (zoom slider) com a percentagem de ajuste atual, os assistentes por página fornecem-lhe os números sem alterar o modo. O PageWidthZoom[N], PageZoom[N] e ActualSizeZoom[N] retornam a percentagem que ajustaria a página N à largura, a ajustaria por completo ou a renderizaria no tamanho real.

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

O que um visualizador concluído realmente necessita

O título original sobrevaloriza o trabalho. O visualizador acima tem algumas dezenas de linhas e já faz o trabalho que um fluxo de trabalho de documentos necessita: abrir um ficheiro, sobreviver a um ficheiro danificado, mostrar uma página, mover-se entre páginas e alterar a ampliação manualmente ou por ajuste. O PDFium faz as partes difíceis silenciosamente. As fontes incorporadas são resolvidas, as anotações e os campos de formulário são desenhados onde o documento os colocou, e a página que vê corresponde à que um utilizador do Chrome veria, porque é o mesmo motor a desenhar ambas.

A partir desta base, as adições são incrementais e não estruturais. A seleção de texto e a pesquisa leem a partir da mesma camada de texto que o PDFium já constrói; metadados como Pdf.Title e Pdf.Author estão à distância de uma leitura de propriedade; a rotação e a escala de cinzentos são opções de renderização que passa ao desenhar uma página num bitmap. Nenhum destes aspetos altera a estrutura principal que tem aqui, que é o objeto do documento, a visualização e o fluxo de carregar e navegar que os liga. Acerte nessa estrutura e o resto são apenas detalhes decorativos.

Os componentes TPdf e TPdfView utilizados ao longo deste artigo fazem parte do PDFium VCL para Delphi e C++Builder, que disponibiliza a referência completa do visualizador na sua página de produto.