Artigo Técnico

Realce Não Destrutivo de PDF em Delphi: a Camada de Revisão do HotPDF

Um retângulo desenhado à volta de um parágrafo durante a revisão não tem de se tornar numa marca dentro do PDF. O THPDFViewerModel do HotPDF expõe AddHighlightRegion, um método que mantém cada realce como um registo em memória, e não como uma alteração ao documento carregado, pelo que um revisor pode marcar dezenas de páginas enquanto o ficheiro em disco permanece byte a byte o mesmo que era. Ampliar para 6400%, rodar a página 90 graus, mudar de Ajustar à Largura para Ajustar à Página, e o mesmo retângulo continua a cair sobre o mesmo parágrafo, porque o cálculo de coordenadas passa pela geometria de renderização real no momento em que a marca foi desenhada

As ferramentas de revisão construídas à volta de um visualizador de PDF esbarram constantemente neste problema. Um ecrã de revisão com marcas, uma passagem de QA sobre faturas geradas, um fluxo interno de aprovação: todos precisam de permitir que alguém chame a atenção para uma região de uma página sem que cada marca provisória se torne numa alteração permanente ao ficheiro, e sem recorrer a um subsistema de anotações completo só para mostrar uma caixa colorida enquanto alguém ainda está a decidir se a marca deve ficar. O HotPDF responde a isto com uma camada de realce dedicada que se situa inteiramente do lado do Modelo, na divisão descrita em construir um visualizador de PDF personalizado com uma arquitetura MVC em Delphi, o que é também a razão pela qual a mesma lista de realces pode ser conduzida a partir de um teste unitário sem qualquer handle de janela à vista

O que armazena efetivamente o AddHighlightRegion do HotPDF?

AddHighlightRegion armazena exatamente três coisas por marca: um índice de página a partir de zero, um THPDFRectangle em coordenadas de espaço do utilizador PDF, e uma TColor, tudo empacotado como um registo THPDFViewerHighlight dentro de THPDFViewerModel. Chamar Viewer.HighlightRegion(PageIndex, PageRect, clYellow), ou o equivalente Model.AddHighlightRegion, adiciona um destes registos a um array privado e devolve o seu índice, e esse índice é o único identificador que o chamador recebe de volta: não há objeto separado, não há interface com contagem de referências, não há nada a libertar. Todas as outras capacidades descritas neste artigo, desenhar a marca, remapeá-la após uma alteração de zoom, apagá-la, são construídas sobre esse pequeno registo

Todo o retângulo é normalizado e recortado antes de ser aceite. AddHighlightRegion troca as bordas esquerda e direita se um revisor arrastar da direita para a esquerda, troca o topo e a base para um arrastamento ascendente, depois recorta o resultado em função do MediaBox da página, obtido através de GetLoadedPageBox. Um retângulo que acabe com largura zero, altura zero, ou inteiramente fora da página é rejeitado sem mais: o método devolve -1 e nada é adicionado à lista. Esse valor de retorno não é decorativo: um lote de realces reconstruído a partir de um ficheiro de revisão externo, ou a partir de coordenadas obsoletas depois de uma página ter sido substituída, pode perder entradas silenciosamente se o chamador não o verificar

Como permanece um realce alinhado após o zoom ou a rotação?

Um realce permanece alinhado porque o HotPDF o armazena no espaço de página PDF e o reprojeta no espaço de ecrã em cada repintura, em vez de armazenar um retângulo de ecrã que ficaria obsoleto assim que o nível de zoom mudasse. THPDFViewerModel.PagePointToView e o seu inverso, ViewPointToPage, fazem essa projeção em duas fases: primeiro a própria entrada /Rotate da página, depois a ViewRotation independente do Visualizador, que nunca é escrita de volta no PDF e só afeta o que o Visualizador apresenta. Desfazer a transformação ao soltar o rato executa as mesmas duas fases em ordem inversa, o que é o que permite que um realce desenhado a alto zoom numa página rodada 270 graus caia exatamente no sítio certo depois de o revisor repor a vista de volta a Ajustar à Página

O DPI usado nessa projeção importa tanto quanto a rotação. O Visualizador do HotPDF captura o DPI exato do bitmap atualmente no ecrã em FRenderedDPI logo após cada renderização, e ImageMouseUp passa esse mesmo valor para ViewPointToPage, pelo que uma coordenada do rato é sempre convertida usando a resolução com que foi efetivamente desenhada, e não uma resolução recalculada a partir da propriedade de zoom atual. CreatePageSnapshot e os métodos relacionados limitam o DPI a um intervalo de 12 a 2400, mas o caminho de renderização interativa não tem esse teto: a escala de zoom padrão vai até 6400%, o que equivale a bem mais de 2400 DPI na base predefinida de 96 DPI, pelo que reutilizar um limite ao estilo de snapshot para o mapeamento de coordenadas deslocaria cada realce vários pixels no topo do intervalo de zoom. Dois pequenos valores predefinidos completam a interação: um arrastamento inferior a dois pixels em qualquer eixo é tratado como um clique e não produz realce, e o realce não pode começar até pelo menos uma página ter efetivamente sido renderizada, uma vez que FRenderedDPI começa a zero

Ligar o realce interativo a um ecrã de revisão

Ativar o realce interativo é uma tarefa de três propriedades no próprio controlo THPDFViewer: definir InteractionMode como vimHighlight em vez do predefinido vimBrowse, escolher uma HighlightColor, que assume clYellow por predefinição, e tratar OnMarqueeSelect para saber o que o revisor acabou de desenhar. Tudo o resto, capturar o rato, desenhar o retângulo de seleção pontilhado enquanto o revisor arrasta, converter o ponto de soltura de volta para o espaço da página, chamar AddHighlightRegion, acontece dentro do controlo antes de esse evento disparar

type
  TReviewForm = class(TForm)
    Viewer: THPDFViewer;
    ReviewLog: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
      PageIndex: Integer; const PageRect: THPDFRectangle;
      HighlightIndex: Integer);
  end;

// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
  Viewer.PDFDocument := PdfDoc;
  Viewer.InteractionMode := vimHighlight;
  Viewer.HighlightColor := clLime;
  Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;

procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
  PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
  ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
    [PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
     PageRect.Right, PageRect.Top]));
end;

OnMarqueeSelect só dispara para um arrastamento que efetivamente produziu um realce: um clique demasiado pequeno para contar como arrastamento limpa a sobreposição de seleção de imediato, e um arrastamento que caia inteiramente fora da página chega a AddHighlightRegion mas é aí rejeitado da mesma forma que uma chamada programática seria, pelo que o evento permanece silencioso em ambos os casos. Um pormenor de implementação que vale a pena conhecer, caso o realce alguma vez pareça deixar de responder junto às bordas do controlo: a captura do rato pertence ao próprio THPDFViewer, um descendente de TScrollBox, e não ao TImage interno que mostra o bitmap da página, o que é o que permite a um revisor arrastar para além da borda da página renderizada e ainda assim obter uma soltura limpa

Adicionar, remover e reler realces a partir de código

Os realces não têm de vir necessariamente de um arrastamento do rato. Viewer.HighlightRegion(PageIndex, PageRect, Color), que é encaminhado para o mesmo Model.AddHighlightRegion que o arrastamento interativo chama internamente, é público precisamente para que um ecrã de revisão possa reconstruir realces a partir de dados que já possui: comentários carregados de uma base de dados, resultados de uma pesquisa de texto, ou marcas restauradas de uma sessão anterior. Como as coordenadas são simples números de espaço do utilizador PDF, nada neste caminho depende de uma página ter sido previamente renderizada, ao contrário do arrastamento interativo, que precisa que FRenderedDPI já contenha um valor real

var
  I: Integer;
  Item: TPriorComment;    // your own record: PageIndex + PageRect
  NewIndex: Integer;
begin
  for I := 0 to PriorComments.Count - 1 do
  begin
    Item := TPriorComment(PriorComments[I]);
    NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
    if NewIndex < 0 then
      LogWarning('comment %d fell outside the page and was dropped', [I]);
  end;
end;

Remover um único realce é onde o armazenamento apoiado em array se torna visível. RemoveHighlightRegion apaga um registo e desloca cada registo posterior uma posição para trás, para fechar o espaço, o que significa que qualquer índice capturado anteriormente, seja de um evento OnMarqueeSelect ou de uma enumeração prévia, deixa de ser fiável assim que algo à sua frente na lista é removido. OnHighlightChange dispara em cada adição, remoção, e chamada a ClearHighlightRegions, mas não transporta qualquer informação sobre o que mudou, pelo que o padrão seguro é tratá-lo como um sinal para reconstruir do zero seja qual for a lista que um painel de revisão esteja a mostrar, a partir de HighlightCount e TryGetHighlightRegion, em vez de corrigir um índice em cache no próprio sítio

procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
  I: Integer;
  Mark: THPDFViewerHighlight;
begin
  MarkList.Items.Clear;
  for I := 0 to Viewer.Model.HighlightCount - 1 do
    if Viewer.Model.TryGetHighlightRegion(I, Mark) then
      MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
        TObject(I));
end;

Quando deve uma marca tornar-se antes numa verdadeira anotação de Realce?

Uma região de realce deve tornar-se numa anotação real no momento em que precisar de sobreviver para além dessa única instância de THPDFViewer. O HotPDF também expõe AddHighlightAnnotation para uma página nova e AddLoadedHighlightAnnotation para um documento já carregado, e apesar do nome quase idêntico, este é um mecanismo completamente diferente: ambos escrevem uma verdadeira anotação de marcação de texto ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, no array /Annots da página, com /QuadPoints a marcar exatamente a sequência de glifos, e qualquer visualizador de PDF conforme a renderiza assim que o ficheiro é guardado, não apenas o próprio HotPDF. O mesmo limite de mecanismo decide se uma marca sobrevive através de um ciclo de ida e volta via XFDF: uma anotação criada com AddLoadedHighlightAnnotation é um objeto PDF normal que ExportLoadedAnnotationsToXFDF recolhe e entrega ao Acrobat ou a outra ferramenta de revisão como marcação ISO 19444-1, abordado em importar e exportar anotações de PDF como XFDF em Delphi, enquanto uma região adicionada através de AddHighlightRegion é invisível para essa exportação porque nunca chegou a ser escrita no grafo de objetos: só existe enquanto existir o THPDFViewerModel que a criou. A família completa de tipos de anotação de marcação e geométricos disponíveis numa página, e a forma como um retângulo posiciona cada um, é abordada em o artigo sobre anotações de PDF em Delphi com o HotPDF, e a regra prática é simples: manter uma marca descartável enquanto um documento ainda está a ser discutido, e comprometê-la numa anotação assim que uma decisão for final

Onde a camada de realce termina

A camada de realce, pelo seu lado, não faz qualquer tentativa de se parecer com uma caneta marcadora translúcida: RefreshDocument desenha cada região como um retângulo de contorno de dois pixels na sua própria cor, sobreposto ao bitmap da página em cache, da mesma forma que desenha os resultados de pesquisa, em vez de misturar um preenchimento colorido sobre o texto por baixo, pelo que o clássico aspeto de "lavagem" amarela tem de ser pintado em código da aplicação ou remetido para o próprio fluxo de aparência de uma anotação promovida. Uma capacidade que vale a pena reutilizar assim que uma região existe é CreateCurrentPageRegionSnapshot, que recebe o mesmo THPDFRectangle que um realce já transporta e renderiza apenas essa área para um bitmap, útil para anexar uma pequena imagem de pré-visualização a um comentário de revisão sem exportar a página inteira. Uma compilação de revisão não tem de escolher entre os dois mecanismos à partida: por predefinição, cada nova marca fica como uma região THPDFViewerHighlight descartável enquanto uma discussão de comentário permanecer aberta, e só se chama AddLoadedHighlightAnnotation assim que um revisor a resolver, o que mantém o PDF carregado intocado durante o vaivém que produz a maior instabilidade. O controlo de visualizador aqui descrito faz parte do componente HotPDF standard para Delphi e C++Builder, a par do resto das APIs de anotações e formulários referidas acima