Artigo Técnico

Construa um Visualizador de PDF no Delphi com o PDFium

Um visualizador de PDF no Delphi se resume a dois componentes e a conexão entre eles. TPdf é dono do documento: ele abre o arquivo, o descriptografa e responde perguntas sobre contagem de páginas e metadados. TPdfView é o controle visual que desenha as páginas na tela e lida com rolagem, zoom e a página que o usuário está visualizando no momento. O PDFium Component envolve o mesmo mecanismo de renderização contido no Chrome, de modo que os glifos, o anti-aliasing e as cores que você obtém na tela correspondem ao que seus usuários já veem no navegador deles. O trabalho não está na renderização. Está em conectar o objeto de documento à visualização, carregar sem falhar num arquivo danificado ou protegido por senha e dar ao usuário os poucos controles que fazem o visualizador parecer finalizado: virar a página, alterar o zoom, ajustar a página à janela

Este processo de montagem é apresentado na mesma ordem em que é efetivamente construído. Tudo aqui é renderizado uma página por vez, o que atende à maioria dos fluxos de trabalho com documentos. Se você precisar de páginas empilhadas em uma coluna de rolagem contínua, essa é uma decisão de layout diferente e não é a abordada aqui

Conectando o TPdf ao TPdfView

Coloque um TPdf e um TPdfView no formulário e diga ao controle visualizador qual documento deve ser exibido. Essa única atribuição é todo o vínculo entre o documento não visual e o controle que o desenha

Arquitetura de viewer de PDF Delphi em que TPdf possui o documento, TPdfView o pinta, e uma única atribuição de propriedade os liga sobre a DLL PDFium
TPdf possui o documento enquanto TPdfView o pinta, e uma única atribuição conecta os dois sobre a engine PDFium compartilhada
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf e PdfView foram colocados no formulário em tempo de design.
  PdfView.Pdf := Pdf;                 // a visualização desenha o que quer que este documento contenha
  PdfView.FitMode := pfmFitWidth;     // inicia o usuário em um zoom sensato
end;

Antes de tudo isso rodar, a biblioteca nativa do PDFium precisa estar na máquina. O PDFium Component carrega o pdfium.dll tanto no Win32 quanto no Win64; as duas builds compartilham esse nome de arquivo e vêm em pastas separadas DLLs\Win32 e DLLs\Win64, e o documento simplesmente se recusa a abrir se a DLL não for encontrada ou se o bitness dela não corresponder ao seu executável. Envie a DLL que corresponde à sua plataforma-alvo junto do executável, ou coloque-a em um lugar que o carregador do sistema possa localizá-la. As builds com V8 habilitado só existem para PDFs que contêm JavaScript que você quer executar, o que um visualizador simples não tem, então fique com a DLL padrão a não ser que tenha um motivo concreto para não fazê-lo

Carregando um documento sem confiar na entrada

O instinto é envolver o carregamento num try/except e tratar uma exceção lançada como falha. Esse instinto está errado aqui, e errar isso produz um visualizador que parece bom até alguém entregar um arquivo quebrado a ele. Definir Active := True não levanta exceção numa falha de carga. O PDFium Component captura o erro interno e deixa o Active parado em False, então o único jeito honesto de saber se o documento abriu é ler a propriedade de volta depois de defini-la

Fluxo de decisão de carga para um viewer Delphi PDFium em que definir Active nunca levanta exceção, um false silencioso significa senha errada ou arquivo danificado, e uma única repetição de senha se segue
A ativação nunca lança exceção em caso de falha, então o visualizador relê Active e responde a um false silencioso com uma única nova tentativa de senha
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // nunca gera exceção; a falha deixa Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // a visualização controla sua própria página atual
  PdfView.Active := True;             // até a view estar ativa, ela não mantém página nenhuma
  UpdatePageLabel;
end;

A view tem um interruptor Active próprio e, enquanto ele não é True, o controle não segura página nenhuma: abrir o documento e definir só o PageNumber deixa o visualizador em branco. Mais duas coisas merecem atenção. A primeira é que o PageNumber existe em ambos os objetos e os dois são independentes. O Pdf.PageNumber é a noção de página atual do documento; o PdfView.PageNumber é a página que o controle realmente exibe, e é essa que você define para mover o usuário pelo arquivo. Definir um não move o outro, então um visualizador sempre controla a propriedade da view. A segunda é a indexação com base 1: as páginas vão de 1 a Pdf.PageCount, e não de 0, o que surpreende qualquer um acostumado com arrays com base em zero

Manipulando um arquivo criptografado

Os documentos criptografados se integram no mesmo caminho de carga. Se a senha de abertura for definida antes da ativação, o documento será descriptografado ao ser aberto; se estiver errado ou ausente, o Active permanecerá False exatamente como ocorre em um arquivo corrompido. Portanto, a recuperação consiste em solicitar a senha 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;       // deve ser definido 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;
  PdfView.Active := True;
end;

Como a falha é silenciosa tanto para uma senha incorreta quanto para um arquivo danificado, não é possível diferenciar os dois problemas apenas observando a propriedade Active. Na prática, isso é aceitável para um visualizador: o usuário fornecerá a senha correta ou saberá que o arquivo não será aberto, e a mensagem será a mesma em ambos os casos

Paginando pelo documento

Com o documento aberto, a navegação é feita de forma aritmética com o PdfView.PageNumber delimitado pelo Pdf.PageCount. O único trabalho real é a restrição (clamping), para que os botões nunca empurrem a página para fora do intervalo e o primeiro e o último fiquem desativados nas extremidades do arquivo

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 se reduzem 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 recebendo um número inteiro analisado, e o limitador cobre o caso em que o usuário digita 9999 num arquivo de dez páginas. Mantenha a função UpdatePageLabel como a única etapa responsável por exibir algo como "Página 3 de 12", garantindo que a informação visualizada pelo usuário jamais fique descompassada com o que o painel exibe

Zoom: porcentagens explícitas e modos de ajuste

O zoom no TPdfView vem em dois tipos que interagem e compreender essa interação é a diferença entre um controle de zoom que funciona adequadamente e um que dificulta a experiência do usuário. A rota direta é a propriedade Zoom, uma porcentagem em que 100 significa o tamanho real. A outra é o FitMode, que diz ao visualizador para calcular o zoom para você e continuar a recalcular à medida que a janela é redimensionada

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

// deixe a visualização dimensionar a página para a janela, e mantê-la dimensionada ao redimensionar
PdfView.FitMode := pfmFitWidth;   // a largura da página preenche o controle
PdfView.FitMode := pfmFitPage;    // página inteira visível
PdfView.FitMode := pfmActualSize; // 1:1 com os pontos do documento

Aqui está a parte onde as pessoas tropeçam. Atribuir Zoom diretamente faz o FitMode voltar para pfmNone. Esse é o comportamento correto, não um bug: no momento em que o usuário escolhe exatos 150%, a view não pode mais também honrar "ajustar à largura", porque os dois pedidos conflitam. A consequência para a sua UI é que um botão de zoom e um botão de ajustar à página são estados mutuamente exclusivos, e a barra de ferramentas deve tornar o modo ativo visível. Quando o usuário clica em ajustar à página, defina FitMode; quando ele clica num zoom numérico, defina Zoom e deixe que ele limpe o modo de ajuste por conta própria

Se preferir calcular o valor de ajuste você mesmo, talvez para definir um controle deslizante de zoom (zoom slider) com a porcentagem de ajuste atual, os utilitários por página fornecem os números sem alterar o modo. PageWidthZoom[N], PageZoom[N] e ActualSizeZoom[N] retornam a porcentagem que ajustaria a página N à largura, a ajustaria por inteiro ou a renderizaria em tamanho real

// inicializa a exibição de zoom a partir do valor de ajuste à largura da página atual
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

O que um visualizador finalizado realmente precisa

O visualizador acima é de algumas dezenas de linhas e já faz o que um fluxo de trabalho de documentos precisa: abrir um arquivo, resistir a um quebrado, exibir uma página, movimentar-se entre elas e mudar o zoom de forma manual ou ajustada. O PDFium faz as partes pesadas silenciosamente. As fontes integradas são reproduzidas, anotações e os campos de formulário são colocados onde o documento as determinam, e a tela que você visualiza é a mesma de um usuário do navegador Chrome, porque é o mesmo mecanismo (engine) que desenha ambos

A partir dessa base, as adições são incrementais, não estruturais. Seleção de texto e busca leem da mesma camada de texto que o PDFium já constrói; metadados como Pdf.Title e Pdf.Author são uma leitura de propriedade e nada mais; rotação e escala de cinza são opções de renderização que você passa ao desenhar uma página num bitmap. Nada disso muda a espinha dorsal que você tem aqui, que é o objeto de documento, a view e o fluxo de carregar e depois navegar que os conecta. Acerte essa espinha dorsal e o resto é decoração

Os componentes TPdf e TPdfView usados ao longo do texto fazem parte do PDFium Component para Delphi e C++Builder, que carrega a referência completa do visualizador na página do produto