Artigo Técnico

Construa um Visualizador de PDF Interativo no Delphi com o TPDFlibViewer

Incorporar um painel de leitura de PDF em um aplicativo de negócios Delphi não exige a escrita de um loop de renderização, um mecanismo de rolagem ou um modelo de zoom. A biblioteca losLab PDF Library inclui o TPDFlibViewer, um controle VCL que abre um documento com uma única chamada e oferece seleção de texto, rolagem (panning), zoom de seleção (marquee zoom), capturas de tela (snapshots), realce de região, realce de pesquisa de texto completo, interação com anotações e impressão como comportamento integrado. Este artigo aborda as cinco etapas que transformam um formulário vazio em um visualizador funcional: inserir o controle, conectar as ações prontas, adicionar realce de pesquisa, tratar cliques em anotações e imprimir

Por que usar um controle de visualizador em vez de escrever um loop de renderização?

O argumento da divisão em camadas vem primeiro, porque explica o que você realmente está adquirindo. Renderizar uma página para um bitmap representa os 20% fáceis; os outros 80% são tudo o que está ao redor disso: um cache de bitmap de página com limite de descarte, layout de rolagem em páginas de tamanhos diferentes, modos de zoom que se recalculam no redimensionamento, testes de colisão (hit-testing) que mapeiam um pixel do mouse de volta para as coordenadas da página em qualquer zoom e rotação, e agendamento de redesenho que não pisca. O TPDFlibViewer gerencia tudo isso e delega o trabalho de rasterização para o mesmo mecanismo descrito no artigo sobre renderização multi-engine, de modo que a camada de controle e a camada de renderização permaneçam substituíveis separadamente. O código do seu formulário se comunica com um controle Windows rolável, não com um renderizador

Como incorporar um visualizador de PDF em um aplicativo Delphi?

O método TPDFlibViewer.LoadFromFile abre um documento por meio da instância interna TPDFlib do próprio controle e retorna 1 em caso de sucesso, de modo que o visualizador mínimo consiste em uma chamada de construtor, uma atribuição de Parent e um carregamento. Se você já possuir uma instância do TPDFlib em outro local do aplicativo, o AttachLibrary exibirá o documento selecionado em seu lugar, mantendo a propriedade do seu lado

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // rola ao arrastar; um clique simples ainda segue os links
end;

A propriedade ViewerMode seleciona o que o mouse faz, a partir de cinco valores: vmSelect é a ferramenta padrão de seleção de texto, vmHand rola ao arrastar com um cursor de mão enquanto um clique simples ainda segue os links, vmZoom é um zoom de seleção (marquee zoom) que amplia a área arrastada para preencher a janela de visualização, vmSnapshot captura uma região arrastada para a área de transferência (com um evento OnSnapshot e uma opção SnapshotToClipboard) e vmHighlight desenha realces de região translúcidos persistentes. A ferramenta de realce merece uma observação sobre coordenadas: as regiões são armazenadas no espaço de usuário da página, não em pixels do cliente, de modo que as marcações do AddHighlightRegion sobrevivem ao zoom, à rotação da visualização e à navegação de página sem qualquer controle adicional de sua parte. Em uma visualização rotacionada, a direção do arrasto pode inclusive ir contra os eixos da página, o que gera uma largura ou altura negativa após o mapeamento de rotação inversa — o controle normaliza o retângulo para você

Como conectar uma barra de ferramentas PDF no Delphi sem código de integração?

A unit PDFlibViewerActions remove completamente a necessidade de código de integração para a barra de ferramentas. Ela fornece vinte descendentes de TAction prontos para uso que cobrem navegação de página (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), zoom (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), rotação de visualização (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), impressão, caixas de diálogo para abrir e salvar arquivos, cópia de texto selecionado e uma ação de alternância para cada ferramenta de mouse (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode). Cada ação aponta para um controle por meio de sua propriedade publicada Viewer e mantém seu próprio estado: cada ação se desativa quando nenhum documento está aberto, as ações de navegação acompanham a posição da página, a cópia rastreia se existe uma seleção e as ações de modo e ajuste mantêm seu estado marcado (checked) em sincronia com a ferramenta e o modo de zoom ativos. Em tempo de design, todas elas se registram na categoria losLab PDF do editor de Action List; em tempo de execução, as mesmas classes exigem apenas três linhas cada

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

Há uma armadilha de escopo de unit que vale a pena conhecer se você criar seu próprio registro de ações com base nesse padrão. O RegisterActions é declarado em System.Actions, não em Vcl.ActnList, e o erro de compilador que você recebe com apenas Vcl.ActnList na cláusula uses é ativamente enganoso: o literal de array de classe [TAction1, TAction2, ...] passa a ser analisado como um conjunto (set), produzindo uma cadeia de erros E2010 Incompatible types: 'Integer' and 'class of ...' que parecem uma incompatibilidade de tipo em suas classes de ação. A correção é uma única entrada na cláusula uses, mas o diagnóstico aponta para todos os lugares, exceto para ela. Para barras de ferramentas de zoom, o TPDFlibViewer.EffectiveZoomPercent informa o zoom realmente em vigor — incluindo o fator conhecido nos modos de ajuste de largura (fit-width) e ajuste de página (fit-page) — de modo que uma etapa de aproximação (zoom-in) possa se basear na ampliação real atual, em vez de no último valor que você atribuiu

Realce de resultados de pesquisa e a inversão de coordenadas por trás dele

O método HighlightSearchHits pesquisa todo o documento por uma consulta e desenha uma barra translúcida sobre cada correspondência, retornando a contagem de ocorrências; o ClearSearchHighlights, o SearchHighlightCount e a propriedade SearchHighlightColor completam a interface. Como os retângulos de ocorrência são armazenados no espaço da página e reprojetados a cada redesenho, as barras permanecem coladas ao texto correspondente enquanto o usuário rola, aplica zoom e rotaciona a visualização. Se você usar o componente complementar TPDFlibSearchPanel, seu método RunSearch desenha as mesmas barras no visualizador anexado automaticamente por meio da propriedade HighlightInView, que fica ativada por padrão

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // insensível a maiúsculas e minúsculas
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

A parte sutil, e a razão para preferir o caminho integrado em vez de criar o seu próprio a partir dos resultados brutos de pesquisa, é a conversão do sistema de coordenadas. O mecanismo de texto relata os retângulos de correspondência em coordenadas que dependem da configuração Origin da biblioteca: com Origin 0 ou 3, o eixo Y funciona de baixo para cima, como no espaço de usuário nativo do PDF, e com Origin 2 or 3, o eixo X é medido a partir da borda direita. A sobreposição do visualizador funciona no espaço de página de cima para baixo e baseado na esquerda, de modo que cada retângulo de ocorrência deve ser invertido (Y = PageHeight - Bottom e, onde aplicável, X = PageWidth - Right) antes de ser armazenado — considerar a borda superior relatada pelo seu valor nominal sob a origem padrão desenha o realce na posição espelhada verticalmente na página. O TPDFlibViewer realiza essa conversão de acordo com a origem ativa quando registra cada ocorrência e, em seguida, aplica o mapeamento de rotação direta no momento do desenho, razão pela qual as barras se posicionam corretamente em todos os ângulos. A translucidez em si usa a API Windows AlphaBlend com um alfa de origem constante, já que o canvas da VCL não possui composição alfa nativa. As APIs subjacentes de pesquisa de texto e geometria são abordadas em detalhes no artigo sobre pesquisa de texto e enumeração de elementos de página

Cliques em anotações, dicas de ferramentas e exportação de anexos

O TPDFlibViewer trata as anotações como objetos interativos de primeira classe. Clicar em uma anotação que não seja um link dispara o evento OnAnnotClick com o número da página, o índice da anotação baseado em 1 e a string do subtipo; o método AnnotAtPagePoint expõe o mesmo teste de colisão programaticamente e ignora anotações complementares do tipo Popup, para que você sempre obtenha a anotação que o usuário realmente vê. Passar o mouse sobre uma anotação exibe o texto de Contents como a dica (hint) do controle por meio da opção AnnotHints, que fica ativada por padrão e coopera com a propriedade padrão ShowHint da VCL. Para anotações do tipo FileAttachment, o método SaveAnnotAttachmentToFile grava o arquivo incorporado no disco e retorna 1 em caso de sucesso, o que completa um fluxo de trabalho de clique para salvar em poucas linhas

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

Impressão e os limites que você deve planejar

A impressão não necessita de integração separada. O método PrintDoc exibe a caixa de diálogo de impressão padrão do Windows — escolha da impressora, intervalo de páginas, cópias, agrupamento — e passa o trabalho para o mecanismo de impressão da biblioteca, enquanto PrintDoc(False) envia todo o documento para a impressora padrão sem nenhuma interface de usuário. O método PrintDocRange é totalmente programático, exigindo um nome de impressora explícito, página inicial e final, contagem de cópias e sinalizador de agrupamento, que é o ponto de entrada correto para trabalhos de impressão em lote ou acionados pelo servidor a partir da mesma instância do visualizador

Dois limites merecem menção honesta. Primeiro, o TPDFlibViewer é um controle VCL do Windows; a camada do visualizador é excluída de compilações NOVCL, de modo que serviços de console e alvos não VCL devem usar a API de renderização TPDFlib subjacente diretamente. Segundo, o uso headless é intencionalmente suportado, mas com limites: o método CaptureRegion renderiza qualquer região do cliente — páginas mais realces — em um bitmap de propriedade do chamador e funciona em um controle sem pai (unparented) e sem um identificador de janela (window handle), porque o caminho de desenho evita chamadas da VCL que forçariam a criação do handle. Isso torna a verificação em nível de pixel em testes automatizados prática, mas qualquer coisa que realmente precise de um loop de mensagens, como as ferramentas interativas de mouse, ainda pertence a uma sessão de interface de usuário real. Do lado do desempenho, o controle mantém um cache de páginas em memória com um limite configurável via SetCacheLimit e pode persistir páginas renderizadas entre as sessões; o artigo sobre o cache de disco de páginas do visualizador aborda essa camada e seu comportamento de DPI por monitor em detalhes

Cinco etapas, portanto: posicione o controle e chame LoadFromFile, anexe as ações prontas da unit PDFlibViewerActions, chame HighlightSearchHits para pesquisa na página, trate o evento OnAnnotClick para fluxos de trabalho de anotação e exponha o PrintDoc. Cada parte mostrada aqui é fornecida na biblioteca padrão losLab PDF Library para Delphi, onde o controle de visualizador, as classes de ação e o mecanismo de renderização são instalados juntos