Artigo Técnico

Comparação Lado a Lado de PDF em Delphi com o PDFium

Dois documentos abertos ao mesmo tempo, no mesmo número de página, cada um no seu painel com deslocamento: é esse o cerne de um visualizador de comparação. O PDFium Component entrega isto através de um modelo de objetos direto em que TPdf é dono do ficheiro e TPdfView é dono da apresentação. Um documento, um TPdf, um TPdfView. Se quer três painéis, tem três pares. As partes difíceis não são as chamadas à API; são a aritmética da disposição quando a janela é redimensionada e a lógica de sincronização de páginas quando decide que vista deve seguir qual

Disposição do formulário

O formulário VCL contém três contentores TScrollBox lado a lado, cada um com um TPdfView lá dentro alinhado a alClient para que preencha a caixa. Dois componentes TSplitter ficam entre as caixas para que o utilizador possa ajustar as larguras das colunas em tempo de execução. Uma barra de ferramentas por cima dos painéis leva os botões de abertura, os controlos de ampliação e o alternador entre duas e três vistas

O modo de três vistas é um booleano que o formulário guarda internamente. Quando muda, recalcula as larguras e mostra ou esconde a terceira coluna. A abordagem mais simples é limpar todas as propriedades Align, esconder os separadores, e depois definir posições absolutas:

Diagrama da disposição do formulário de um visualizador de comparação de PDF lado a lado em Delphi construído com o PDFium Component, a mostrar uma barra de ferramentas, três caixas de deslocamento com painéis TPdfView, e separadores nos modos de duas e de três vistas
Cada painel é uma caixa de deslocamento com um TPdfView lá dentro, e alternar entre duas e três vistas é apenas um conjunto diferente de atribuições de largura
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Aplicar o mesmo (ClientHeight - altura da barra) aos três valores de Height
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Definir Align := alNone nas três caixas antes da aritmética inteira evita que o motor de restrições da VCL lute contra as suas atribuições. Reponha a visibilidade dos separadores depois de posicionar, se quiser redimensionamento por arrasto no modo de duas vistas

A altura de cada caixa de deslocamento é a área de cliente menos a altura do painel da barra de ferramentas. Como a barra está ancorada ao topo com alTop, ClientHeight - PanelButtons.Height dá-lhe o espaço vertical utilizável. Atribua isto às três caixas dentro da mesma chamada a UpdateLayout, para que nunca haja um fotograma em que uma caixa fique mais alta do que as outras e provoque uma tremura na disposição

Abrir um documento

Cada par de painéis precisa do seu próprio procedimento de abertura. O padrão é curto: desativar o componente, definir o nome do ficheiro, ativar, e depois verificar Active; se tiver ficado a False, pedir uma palavra-passe e tentar de novo. Repare que TPdfView.Active é o que controla a renderização, mas TPdf.Active é o que abre efetivamente o ficheiro; são independentes. Pôr PdfView.Active := True quando o TPdf ligado ainda não está ativo é inócuo mas não mostra nada

Fluxograma da abertura de um documento PDF com o PDFium Component em Delphi, a mostrar a verificação silenciosa de Active, uma tentativa com palavra-passe, e uma caixa de erro para ficheiros danificados ou protegidos
Um carregamento falhado deixa Active a False sem levantar exceção, pelo que o fluxo o verifica, tenta uma vez com palavra-passe, e por fim reporta o problema em vez de mostrar um painel em branco
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // As falhas de carregamento são silenciosas: Active fica False sem exceção.
  if not PdfComponent.Active then
  begin
    // Provavelmente um ficheiro protegido; dê ao utilizador uma tentativa.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

Verifique sempre PdfComponent.Active depois da atribuição; um ficheiro danificado ou uma palavra-passe errada fazem o carregamento falhar em silêncio, sem levantar exceção no caminho por omissão. Definir explicitamente PdfViewComponent.PageNumber := 1 depois de uma abertura bem-sucedida evita arrastar um número de página desatualizado do documento anterior

A caixa de mensagem no fim é intencional: quer que os ficheiros corrompidos ou não suportados apareçam de imediato em vez de serem engolidos como um painel branco e silencioso. Um utilizador que não vê nada não faz ideia se o ficheiro carregou e está simplesmente vazio, ou se o componente o rejeitou. Reportar a falha mantém o erro visível

Seguir o painel ativo

Quando o utilizador clica dentro de um painel, esse painel passa a ativo. O formulário guarda um campo privado FActivePdfView: TPdfView. O retorno visual é uma mudança de cor de contorno na TScrollBox que o contém: ponha-a a clHighlight na ativa e a clWindow nas outras. Ligue isto ao TPdfView.OnClick de cada painel e também ao procedimento de abertura, para que o foco acompanhe o documento que acabou de abrir

Algumas operações aplicam-se a todos os painéis visíveis e não apenas ao ativo. Um booleano FAllViewsMode no formulário comanda esse ramo. Quando está a verdadeiro, as mudanças de ampliação e a navegação de páginas abrem-se em leque para todos os painéis que tenham um documento ativo:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Navegação de páginas sincronizada

A navegação sincronizada é opcional mas útil em fluxos de revisão de documentos em que ambos os ficheiros cobrem o mesmo intervalo de páginas. A lógica pertence a um tratador de evento que dispara depois de o utilizador navegar numa das vistas. Quando uma vista de origem muda o seu PageNumber, o tratador propaga esse número às outras vistas, sujeito a uma guarda: a vista de destino tem de ter pelo menos esse número de páginas, caso contrário salta

O PageNumber de TPdfView e o de TPdf são independentes. TPdf.PageNumber acompanha a página que o componente de documento considera atual; TPdfView.PageNumber acompanha o que está apresentado no ecrã. Para efeitos de navegação quer a propriedade da vista, não a do documento

Uma caixa de verificação com um rótulo do género "Sincronizar páginas" dá o controlo ao utilizador. Quando está desmarcada, cada painel navega de forma independente e o tratador sai de imediato. Essa independência é importante para casos em que os dois documentos têm contagens de páginas diferentes, ou em que o utilizador quer encontrar a passagem equivalente numa tradução que começa noutra página. Forçar a sincronização sempre tornaria a ferramenta mais difícil de usar do que uma simples disposição de duas janelas no ambiente de trabalho

Uma coisa a vigiar: definir PdfView.PageNumber por código dentro do tratador de sincronização vai, por si só, disparar o evento de mudança nessa vista. Proteja-se contra a recursão infinita com uma flag booleana que ativa antes da atribuição e limpa logo a seguir. A flag é por formulário, não por vista, porque as três vistas partilham o mesmo tratador

Diagrama da navegação de páginas sincronizada num visualizador de comparação de PDF em Delphi com o PDFium Component, com a caixa de verificação de sincronização, uma guarda de contagem de páginas por vista de destino, e uma flag de proteção contra recursão
O número de página viaja da vista de origem para todas as outras vistas apenas quando a sincronização está ativa e cada vista de destino contém de facto essa página

Ampliação por painel

Cada TPdfView transporta a sua própria propriedade Zoom, um Double em percentagem em que Zoom := 100 significa tamanho real (100%). Defini-la sobrepõe-se a qualquer FitMode ativo. Para um botão de ajustar à largura no painel ativo, leia a ampliação de ajuste em PdfView.PageWidthZoom[PdfView.PageNumber] e atribua-a. Para ajustar à página, use PageZoom[PageNumber]. Ambas são propriedades de vetor indexadas por número de página a começar em 1, pelo que deve proteger-se contra um número de página igual a zero antes de lhes aceder

Quando exporta a página atual para uma imagem, leia a rotação a partir da vista mas chame RenderPage no componente TPdf, não na vista. A forma de bitmap de TPdf.RenderPage recebe dimensões explícitas em pixels mais um valor TRotation e um conjunto TRenderOptions. A variante de função devolve um TBitmap cuja propriedade é de quem chama e que liberta por si depois de gravar:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

O multiplicador de 2x na largura e na altura dá saída mais nítida em documentos com texto miúdo. O try/finally à volta da libertação do bitmap não é opcional; um cancelamento em TSaveDialog continua a passar pelo bloco finally, e quer o bitmap libertado independentemente do que o utilizador tenha feito

Requisitos de DLL

O PDFium Component envolve a biblioteca nativa pdfium. Um processo anfitrião de 32 bits precisa de pdfium32.dll; um de 64 bits precisa de pdfium64.dll. As variantes com o motor de JavaScript V8 acrescentam o sufixo v8 e pesam aproximadamente 23-27 MB, contra os 5-6 MB das compilações normais. Para um visualizador de comparação que desativa o preenchimento de formulários (Pdf.FormFill := False), a compilação normal sem V8 chega e mantém a distribuição mais pequena

Coloque a DLL no mesmo diretório do executável, ou em qualquer diretório do PATH do sistema. O componente carrega-a a pedido quando o primeiro TPdf é ativado, pelo que uma DLL em falta só se manifesta nesse ponto e não no arranque da aplicação. Se distribuir um instalador, a abordagem mais fiável é copiar a DLL para a pasta da aplicação durante a instalação em vez de contar com um diretório de sistema que um administrador possa mais tarde limpar

As compilações com V8 são úteis sobretudo quando precisa de interagir com ações de JavaScript do PDF, por exemplo para acionar campos de cálculo ou tratadores de submissão. Um visualizador de comparação passivo não tem razão nenhuma para correr JavaScript; pôr Pdf.FormFill := False antes de Active := True salta por completo o ambiente de preenchimento de formulários, o que também significa que nenhum motor de JS é inicializado mesmo que use a compilação normal. Esse é o comportamento por omissão correto para um visualizador só de leitura, seja qual for a variante de DLL que distribua

Para mais detalhes sobre o PDFium Component e a sua API completa, visite a página de produto do Delphi PDFium Component