Artigo Técnico

Construa um Visualizador de PDF no Delphi com o PDFium Component

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

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 que qualquer uma dessas execuções aconteça, a biblioteca nativa do PDFium deve estar na máquina. O PDFium Component chama o pdfium32.dll ou pdfium64.dll, dependendo da plataforma-alvo (target platform), e o documento simplesmente se recusa a abrir se a DLL não puder ser encontrada. Envie a DLL correspondente ao lado de seu executável, ou coloque-a em um lugar que o carregador do sistema possa localizá-la. As construções habilitadas para V8 só existem para PDFs que contêm JavaScript a ser executado, o que um simples visualizador não faz, por isso, procure a DLL padrão, a não ser que haja uma razão para não fazê-lo

Carregando um documento sem confiar na entrada

O instinto é envolver o carregamento em um try/except e tratar uma exceção lançada como falha. Esse instinto está errado aqui, e entender isso errado produz um visualizador que parece bom até que alguém lhe entregue um arquivo quebrado. A definição Active := True não é acionada em caso de falha de carregamento. O PDFium Component detecta o erro interno e deixa Active definido como False, então a única maneira honesta de saber se o documento foi aberto é ler a propriedade novamente depois que você 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;

Duas coisas merecem atenção. A primeira é que o PageNumber existe em ambos os objetos e os dois são independentes. Pdf.PageNumber é a noção do documento de uma página atual; PdfView.PageNumber é a página que o controle visual realmente exibe, e é essa que você define para mover o usuário através do arquivo. A configuração de uma não move a outra, portanto, um visualizador sempre controla a propriedade da exibição. 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;       // 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 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;

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

// 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 é onde a maioria das pessoas tropeçam. Ao atribuir diretamente o Zoom, a função FitMode é zerada para a opção pfmNone. Esse é o comportamento correto, não um bug: a partir do momento em que o usuário escolhe 150% exatos, o visor não pode mais honrar a função "ajustar a largura" (fit to width), porque as duas demandas entram em conflito. A consequência para a UI (Interface do Usuário) é que um botão de aproximação de tela e um botão para ajustar a tela são estados mutuamente exclusivos, e a barra de ferramentas deve tornar o modo ativo visível. Ao clicar no ajuste de tela, ative a opção FitMode; se preferir ajustar a tela numericamente, acione Zoom e deixe que ela descarte por conta própria o modo de ajuste de tela

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

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

Dessa base as complementações são incrementais ao invés de serem baseadas em sua formação. O recurso de busca e seleção de texto baseiam-se na mesma camada de texto que o PDFium já concebe; detalhes operacionais como o Pdf.Title e Pdf.Author dependem de apenas uma leitura adicional; bem como os modos de visualização em preto, branco, ou inclinado, que você altera enquanto os converte para uma resolução em pixel. Isso não modifica as funções principais do programa, o documento que está operando, as imagens exibidas e o procedimento contínuo que os integra. Execute a base do programa perfeitamente e todo o resto é enfeite

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