Artigo Técnico

Criar um Visualizador de PDF em Delphi com o PDFium Component

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 Component 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

Arquitetura de visualizador PDF Delphi em que o TPdf possui o documento, o TPdfView o pinta e uma atribuição de propriedade os liga por cima da DLL PDFium
TPdf possui o documento enquanto TPdfView o pinta, e uma única atribuição liga os dois sobre o motor PDFium partilhado
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // O Pdf e o PdfView foram colocados no design time.
  PdfView.Pdf := Pdf;                 // a vista pinta o que quer que este documento contenha
  PdfView.FitMode := pfmFitWidth;     // começa com o utilizador num zoom sensato
end;

Antes de tudo isto ser executado, a biblioteca nativa do PDFium tem de estar na máquina. O PDFium Component 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 Component 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

Fluxo de decisão de carga para um visualizador PDFium Delphi em que definir Active nunca gera exceção, um false silencioso significa uma palavra-passe errada ou um ficheiro danificado, e se segue uma repetição da palavra-passe
A ativação nunca levanta exceção em caso de falha, pelo que o visualizador lê Active de volta e responde a um falso silencioso com uma única nova tentativa de palavra-passe
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // nunca gera exceções; a falha deixa Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // a vista controla a sua própria página atual
  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;       // tem de ser definida antes de 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;

// os quatro botões de navegação reduzem-se a uma chamada cada
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

Interação entre Zoom e FitMode num visualizador PDFium Delphi em que atribuir um Zoom exato repõe o FitMode em pfmNone e escolher um modo de ajuste devolve o zoom à vista
Atribuir um zoom exato limpa o modo de ajuste, e escolher um modo de ajuste devolve o cálculo do zoom à vista
// ampliações fixas
PdfView.Zoom := 100;     // tamanho real
PdfView.Zoom := 50;      // metade
PdfView.Zoom := 200;     // dobro

// deixa a vista dimensionar a página na janela e mantê-la dimensionada ao redimensionar
PdfView.FitMode := pfmFitWidth;   // a largura da página preenche o controlo
PdfView.FitMode := pfmFitPage;    // página inteira visível
PdfView.FitMode := pfmActualSize; // 1:1 com os pontos do documento

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

// inicializa uma leitura de zoom a partir do valor de ajustar à largura da página atual
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 Component para Delphi e C++Builder, que disponibiliza a referência completa do visualizador na sua página de produto