Technical Article

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

Dois documentos abertos em simultâneo, com o mesmo número de página, cada um no seu próprio painel com barra de rolagem: este é o núcleo de um visualizador de comparação. O PDFium VCL disponibiliza isto através de um modelo de objetos simples no qual TPdf detém o ficheiro e TPdfView controla a exibição. Um documento, um TPdf, um TPdfView. Se pretender três painéis, terá três pares. A parte complexa não reside nas chamadas de API, mas sim na aritmética de layout quando a janela é redimensionada e na lógica de sincronização de página ao decidir qual a visualização que deve acompanhar a outra.

Layout do Formulário

O formulário VCL contém três contentores TScrollBox lado a lado, cada um com um TPdfView interno alinhado como alClient para preencher a caixa. Dois componentes TSplitter situam-se entre as caixas para que o utilizador possa ajustar a largura das colunas em tempo de execução. Uma barra de ferramentas acima dos painéis disponibiliza os botões de abertura, controlos de zoom e a alternância entre a visualização de dois ou três painéis.

O modo de três visualizações é um booleano que o formulário monitoriza internamente. Quando este é alternado, recalcula as larguras e mostra ou oculta a terceira coluna. A abordagem mais simples consiste em limpar todas as propriedades Align, ocultar os splitters e definir posições absolutas:

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;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  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 de inteiros evita conflitos com o motor de restrições do VCL durante as atribuições. Restaure a visibilidade do splitter após o posicionamento se pretender permitir o redimensionamento por arrastamento no modo de duas colunas.

A altura de cada caixa de rolagem corresponde à área cliente menos a altura da barra de ferramentas. Como a barra de ferramentas está acoplada no topo com alTop, a operação ClientHeight - PanelButtons.Height indica o espaço vertical disponível. Atribua este valor às três caixas na mesma chamada de UpdateLayout para evitar que uma caixa fique temporariamente mais alta do que as outras, causando cintilação no layout.

Abrir um Documento

Cada par de painéis necessita do seu próprio procedimento de abertura. O padrão é simples: desative o componente, defina o nome do ficheiro, tente ativar e capture a exceção EPdfError caso o ficheiro requeira palavra-passe. Note que TPdfView.Active controla a renderização, mas TPdf.Active é o que realmente abre o ficheiro; estes são independentes. Definir PdfView.Active := True quando o TPdf associado não está ativo é inofensivo, mas nada exibe no ecrã.

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 := '';

  try
    PdfComponent.Active := True;
  except
    on E: EPdfError do
    begin
      if InputQuery('Password', 'Enter document password:', Password) then
      begin
        PdfComponent.Password := Password;
        PdfComponent.Active   := True;
      end
      else
        raise;
    end;
  end;

  if PdfComponent.Active then
  begin
    PdfViewComponent.PageNumber := 1;
    SetActivePdfView(PdfViewComponent);
  end;
end;

Verifique sempre PdfComponent.Active após a atribuição; um ficheiro danificado ou uma palavra-passe incorreta faz com que o carregamento falhe silenciosamente sem lançar exceções no fluxo padrão. Definir explicitamente PdfViewComponent.PageNumber := 1 após uma abertura bem-sucedida evita a retenção de um número de página obsoleto do documento anterior.

O código de tratamento de palavra-passe acima lança uma exceção para qualquer erro que não seja a mensagem de palavra-passe conhecida. Isto é intencional: pretende-se que ficheiros corrompidos ou não suportados sejam assinalados de imediato, em vez de serem ocultados sob um painel em branco. Um utilizador que não veja nada não sabe se o ficheiro foi carregado e está vazio ou se o componente o rejeitou. Lançar a exceção mantém o erro visível.

Monitorização do Painel Ativo

Quando o utilizador clica dentro de um painel, este torna-se o painel ativo. O formulário monitoriza o campo privado FActivePdfView: TPdfView. O feedback visual consiste na alteração da cor do contorno do TScrollBox correspondente: defina-a como clHighlight para o ativo e clWindow para os restantes. Associe esta lógica a cada evento TPdfView.OnClick e 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 controla essa ramificação. Quando ativo, as alterações de zoom e a navegação de página estendem-se a 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ágina Sincronizada

A navegação sincronizada é opcional, mas útil em fluxos de revisão de documentos em que ambos os ficheiros abrangem o mesmo intervalo de páginas. A lógica reside num processador de eventos (event handler) acionado após o utilizador navegar numa visualização. Quando a visualização de origem altera a sua PageNumber, o processador propaga esse número para as outras visualizações, com uma salvaguarda: a visualização de destino tem de ter pelo menos esse número de páginas, caso contrário ignora.

As propriedades PageNumber no TPdfView e no TPdf são independentes. A propriedade TPdf.PageNumber indica a página que o componente de documento considera atual; TPdfView.PageNumber indica o que está visível no ecrã. Para fins de navegação, deve utilizar a propriedade da visualização e não a do documento.

Uma caixa de seleção etiquetada como "Sincronizar páginas" dá o controlo ao utilizador. Quando desmarcada, cada painel navega de forma independente e o processador termina imediatamente. Esta independência é importante para casos em que os dois documentos têm contagens de páginas diferentes, ou quando o utilizador pretende encontrar a passagem correspondente numa tradução que se inicia numa página diferente. Impor sempre a sincronização tornaria a ferramenta mais difícil de usar do que uma disposição simples de duas janelas no ambiente de trabalho.

Um aspeto a acautelar: definir programaticamente PdfView.PageNumber dentro do processador de sincronização irá acionar o evento de alteração nessa mesma visualização. Evite a recursão infinita com uma flag booleana ativada antes da atribuição e desativada imediatamente após a mesma. A flag é ao nível do formulário e não por visualização, dado que as três visualizações partilham o mesmo processador.

Zoom Individual por Painel

Cada TPdfView possui a sua própria propriedade Zoom, um valor do tipo Double em percentagem no qual 1.0 corresponde a 100%. A sua definição sobrepõe-se a qualquer FitMode ativo. Para um botão de ajuste à largura no painel ativo, obtenha o zoom adequado de PdfView.PageWidthZoom[PdfView.PageNumber] e atribua-o. Para o ajuste à página, utilize PageZoom[PageNumber]. Ambas são propriedades de matriz indexadas pelo número da página baseada em 1, pelo que deve prevenir o acesso com número de página igual a zero.

Ao exportar a página atual para imagem, obtenha a rotação a partir da visualização mas chame RenderPage no componente TPdf, e não na visualização. A variante bitmap de TPdf.RenderPage requer dimensões de píxeis explícitas, um valor TRotation e um conjunto TRenderOptions. Esta variante da função devolve um TBitmap que deve libertar manualmente após a gravação:

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 altura proporciona um resultado mais nítido para documentos com texto pequeno. O bloco try/finally em torno da libertação do bitmap é obrigatório; o cancelamento de um TSaveDialog continuará a acionar o bloco finally, pretendendo-se que o recurso do bitmap seja sempre libertado.

Requisitos de DLL

O PDFium VCL encapsula a biblioteca nativa pdfium. Um processo anfitrião de 32 bits necessita do ficheiro pdfium32.dll; um anfitrião de 64 bits necessita de pdfium64.dll. As variantes com o motor de JavaScript V8 incluem o sufixo v8 e ocupam cerca de 23-27 MB, em comparação com os 5-6 MB das compilações padrão. Para um visualizador de comparação que desative o preenchimento de formulários (Pdf.FormFill := False), a compilação padrão sem V8 é suficiente e reduz o tamanho da distribuição.

Coloque a DLL no mesmo diretório do executável ou em qualquer diretório incluído no PATH do sistema. O componente carrega-a a pedido quando o primeiro TPdf é ativado, de forma que a falta da DLL se manifesta nesse momento e não no arranque da aplicação. Se distribuir um instalador, a abordagem mais segura consiste em copiar a DLL para a pasta da aplicação durante a instalação, em vez de depender de diretórios do sistema que possam ser limpos por um administrador.

As compilações V8 são úteis principalmente quando necessita de interagir com ações de JavaScript do PDF, por exemplo, para acionar campos de cálculo ou processadores de submissão. Um visualizador de comparação passivo não tem razões para executar JavaScript; definir Pdf.FormFill := False antes de Active := True contorna inteiramente o ambiente de formulários, o que significa que nenhum motor de JS é inicializado mesmo que se utilize a compilação padrão. Este é o comportamento padrão correto para um visualizador apenas de leitura, independentemente da variante de DLL fornecida.

Para obter mais detalhes sobre o componente PDFium VCL e a sua API completa, visite a página do Componente Delphi PDFium VCL.