A comparação visual do PDFlibPas renderiza dois PDFs no mesmo DPI e faz o diff do que um leitor realmente vê. O TPDFlib.CompareRenderedPagesWith compara um par de páginas e retorna pixels alterados, regiões, uma máscara e um overlay. O TPDFlib.CompareDocumentVisualsWith e o TPDFlib.CompareFileVisuals pareiam documentos inteiros página a página, detectam páginas inseridas e removidas e reportam tudo como JSON
O caso de uso é teste de regressão visual. Alguém reescreve o template de fatura, atualiza um pacote de fontes ou troca o motor de renderização, e a pergunta não é se os bytes mudaram (sempre mudam) e sim se algum cliente vai ver diferença. Um diff estrutural responde outra pergunta. Dois arquivos podem diferir em todo content stream e ainda pintar páginas idênticas, e um único operando deslocado pode mover um total em três pontos sem tocar em nada que um extrator de texto sinalizaria. Pixels são a única camada em que "parece o mesmo" é testável
O que um diff visual de PDF realmente compara?
O PDFlibPas compara o resultado visível sobre uma página branca, não o conteúdo bruto dos buffers. Toda página é renderizada via RenderPageToString como um BMP descomprimido, decodificada em um bitmap top-down, e qualquer pixel que carregue alpha é compostado sobre branco antes que os canais sejam comparados. Um bitmap RGB e um bitmap RGBA que parecem idênticos na tela, portanto, comparam como idênticos, que é exatamente o que você quer quando os dois lados vieram de caminhos de renderização diferentes
O decodificador BMP tem uma peculiaridade deliberada que vale conhecer. Um bitmap de 32 bits BI_RGB carrega um quarto byte por pixel, mas o formato deixa esse byte indefinido: alguns geradores o preenchem com 0, outros com 255, outros com sobras. O PLDecodeVisualBMP o ignora e mantém a cor. Tratar esse byte como opacidade permitiria que um renderizador que escreve zeros apagasse cada pixel da página, e o diff reportaria felizmente dois documentos em branco e idênticos. Se você alimenta a API de nível mais baixo com os seus próprios bitmaps, o alpha só é honrado quando você mesmo declara o formato como vpfRGBA ou vpfBGRA
Um pixel conta como alterado quando qualquer um dos seus canais vermelho, verde ou azul difere em mais do que ColorTolerance. Tolerância 0 significa bit-exato. Na prática, um valor de um dígito absorve o jitter de anti-aliasing entre builds do renderizador sem esconder um glifo alterado, porque uma mudança real de texto move os canais em muito mais do que isso. Escolha o número a partir de uma execução de baseline sabidamente boa, não por chute
Comparando um único par de páginas com CompareRenderedPagesWith
O TPDFlib.CompareRenderedPagesWith é a ferramenta quando você já sabe quais páginas pertencem juntas e quer detalhe: uma bounding box, regiões de mudança conectadas, uma máscara opcional por pixel e um overlay opcional tingido de vermelho. O record de opções e os tipos de resultado moram na unit PDFlibVisualCompare, e o PLDefaultVisualCompareOptions entrega tolerância 0, tiles de 32 pixels, nenhuma região ignorada e nenhuma máscara ou overlay
uses
PDFlibrary, PDFlibVisualCompare;
var
Baseline, Candidate: TPDFlib;
Options: TPDFVisualCompareOptions;
Comparison: TPDFVisualCompareResult;
ErrorText: WideString;
begin
Baseline := TPDFlib.Create;
Candidate := TPDFlib.Create;
try
if (Baseline.LoadFromFile('invoice-v1.pdf', '') = 1) and
(Candidate.LoadFromFile('invoice-v2.pdf', '') = 1) then
begin
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.IncludeOverlay := True;
// Mascare o timestamp de impressão; unidades em pixels no DPI abaixo
SetLength(Options.IgnoreRects, 1);
Options.IgnoreRects[0].Left := 600;
Options.IgnoreRects[0].Top := 20;
Options.IgnoreRects[0].Right := 790;
Options.IgnoreRects[0].Bottom := 50;
if Baseline.CompareRenderedPagesWith(Candidate, 1, 1, 96, Options,
Comparison, ErrorText) then
begin
if Comparison.ChangedPixels > 0 then
Writeln(PLVisualCompareResultToJSON(Comparison));
end
else
Writeln('Compare failed: ', ErrorText);
end;
finally
Candidate.Free;
Baseline.Free;
end;
end;
Três detalhes mordem as pessoas aqui. Os retângulos ignorados estão em pixels renderizados com origem no canto superior esquerdo e intervalos meio abertos, então Right e Bottom são exclusivos e o mesmo retângulo cobre uma parte diferente da página se você mudar o DPI. Retângulos ignorados sobrepostos contam uma vez, e pixels ignorados aparecem em IgnoredPixels em vez de sumir, o que mantém o ChangedRatio honesto porque ele divide apenas por pixels comparados. Por fim, a chamada de página única exige que os dois renders tenham as mesmas dimensões; uma página que mudou de tamanho falha com erro em vez de ser esticada para caber
As regiões vêm de uma passada de tiles: pixels alterados são agrupados em quadrados de TileSize e tiles adjacentes são fundidos em uma região com os seus próprios limites e contagem de pixels. Tiles maiores dão menos regiões, mais grosseiras, o que geralmente é o que um relatório de teste quer. O overlay é uma cópia RGB da segunda página com os pixels alterados tingidos de vermelho, retornada como bytes brutos em Comparison.Overlay; escrevê-lo como PNG para os seus artefatos de CI é código seu, não da biblioteca
Como o pareamento de páginas em nível de documento lida com páginas inseridas?
O TPDFlib.CompareDocumentVisualsWith pareia páginas com uma longest common subsequence sobre fingerprints exatos de pixels, de modo que uma página inserida não transforme toda página seguinte em uma falsa mudança. Um loop ingênuo de página 1 com página 1, página 2 com página 2 é inútil no momento em que um template ganha uma folha de capa: toda página depois da inserção fica deslocada em um, e o relatório acende em vermelho de cima a baixo
Cada página é renderizada e reduzida a uma chave feita da sua largura em pixels, altura e um digest SHA-256 dos pixels visíveis. O tamanho importa mais do que parece: uma página retrato toda branca e uma página paisagem toda branca de mesma área têm buffers de pixels byte-idênticos, e sem as dimensões na chave elas ancorariam uma na outra. Chaves idênticas dos dois lados viram âncoras, o LCS escolhe a mais longa cadeia ordenada delas, e as páginas entre duas âncoras consecutivas são pareadas em ordem. Quando um vão guarda mais páginas de um lado, o excedente é reportado como added ou removed
var
Options: TPDFVisualCompareOptions;
Report: WideString;
begin
// Baseline e Candidate carregados como antes, com settings de render equivalentes
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.TileSize := 64;
Report := Baseline.CompareDocumentVisualsWith(Candidate, 96, Options);
if Baseline.LastErrorCode <> 0 then
Writeln('Visual diff failed: ', Report)
else
Writeln(Report);
end;
As âncoras são bit-exatas, e isso é uma escolha de design, não um descuido. O ColorTolerance se aplica apenas à comparação de páginas pareadas, nunca ao fingerprint. Uma página que não tem nada além de ruído de anti-aliasing não vai ancorar, mas ainda cai em um vão, é pareada em ordem com a sua correspondente e então compara como unchanged sob tolerância. Âncoras fuzzy seriam mais tolerantes e bem menos previsíveis, porque páginas quase duplicadas (separadores em branco, páginas de termos repetidas) começariam a casar umas com as outras
Esteja claro sobre o que esse alinhamento é. O JSON o chama de exact-raster-lcs-gap-order de propósito: é um alinhamento de raster, não reconhecimento semântico de páginas. Se um vão contém uma página reescrita e uma página inserida, o pareamento em ordem pode comparar a reescrita contra a inserção e reportar a original como removida. Documentos cheios de páginas idênticas são igualmente ambíguos. Quando o pareamento importa para um veredito, olhe os pares, não apenas os contadores
Lendo o relatório JSON
O relatório de documento tem uma forma fixa em que você pode fazer asserts em um test runner. Em caso de sucesso ele carrega "status":"complete", "complete":true, o nome do alinhamento, firstPageCount, secondPageCount e os contadores changedPages, addedPages, removedPages e unchangedPages. O array pairs então lista cada linha com firstPage, secondPage (0 para o lado ausente) e um kind de unchanged, changed, size_changed, added ou removed
Linhas de kind changed e unchanged embutem um objeto comparison com width, height, comparedPixels, changedPixels, ignoredPixels, changedRatio, boundingBox (null quando nada mudou) e uma lista regions de bounds mais changedPixels. Uma linha size_changed não tem comparação nenhuma; ela reporta firstWidth, firstHeight, secondWidth e secondHeight e conta para changedPages. Esticar uma página redimensionada para compará-la produziria uma parede de pixels alterados sem significado, então a biblioteca reporta a mudança de geometria e para por aí
O relatório de documento nunca contém máscaras nem overlays. O CompareDocumentVisualsWith força IncludeMask e IncludeOverlay para off mesmo que as suas opções peçam, de modo que a memória fica plana conforme a contagem de páginas cresce em vez de guardar uma prévia de página inteira para cada par. Use o relatório para achar as páginas interessantes, depois chame CompareRenderedPagesWith apenas nessas páginas para obter o overlay. Falhas voltam como "status":"error", "complete":false com uma string error, e o LastErrorCode é definido como 401
Comparando contra um arquivo com CompareFileVisuals
O TPDFlib.CompareFileVisuals é a forma de chamada única para CI: ele carrega o outro PDF, copia as configurações atuais do renderizador sobre ele e retorna o mesmo JSON de documento. A assinatura é CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance), em que a tolerância deve ser de 0 a 255 e o DPI deve ser finito e positivo
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('baseline\statement.pdf', '') = 1 then
begin
Report := Lib.CompareFileVisuals('build\statement.pdf', '', 96, 8);
Writeln(Report);
end;
finally
Lib.Free;
end;
end;
A herança do renderizador é o motivo para preferir essa chamada em vez de carregar o segundo arquivo você mesmo. O documento carregado assume o render scale, o tipo de crop e as configurações gráficas da instância atual, mais a escolha de engine Cairo ou PDFium quando essas estão compiladas. Se você constrói duas instâncias de TPDFlib à mão para o CompareDocumentVisualsWith, configurar ambas identicamente é problema seu, e um descasamento aparece como toda página alterada. Ela também não aceita regiões ignoradas; recorra à API tipada quando precisar delas
Limites, custo e onde o diff estrutural se encaixa
O PDFlibPas delimita o trabalho explicitamente para que uma entrada hostil ou enorme falhe com erro em vez de esgotar a memória. Uma página renderizada não pode exceder 64 Mi pixels, a grade de tiles tem teto de 4 Mi tiles, e o alinhamento de páginas recusa entradas em que a soma das duas contagens de páginas passa de 8192 ou a matriz do LCS excederia 4 Mi células. A comparação de documentos também renderiza cada página duas vezes, uma para o fingerprint e outra para o compare pareado, então faça esse orçamento em 150 DPI ou mais. Para a maioria das suítes de regressão, 72 a 96 DPI pegam mudanças de layout e de texto em uma fração do tempo
Diff visual e diff estrutural respondem perguntas diferentes, e um pipeline sério geralmente quer os dois. O compare de pixels diz se um leitor notaria algo. Ele não consegue dizer se um documento assinado ganhou uma anotação que renderiza fora da página, ou se um update incremental trocou silenciosamente o valor de um campo de formulário pelo mesmo texto visível. Para essa camada, classificar o que mudou em um PDF depois que ele foi assinado percorre o diff em nível de revisão. E como a escolha do renderizador comanda os pixels, renderização PDF multi-engine em Delphi vale a leitura antes de você fixar uma baseline em um engine
FAQ
A comparação visual do PDFlibPas estica páginas que mudaram de tamanho?
Não. O CompareRenderedPagesWith retorna erro para páginas de tamanho de pixel diferente, e o relatório de documento marca esses pares como size_changed com ambas as dimensões em vez de compará-los
Por que páginas quase idênticas não são usadas como âncoras?
Âncoras usam um fingerprint SHA-256 exato dos pixels visíveis mais o tamanho da página, de modo que o alinhamento fica previsível. A tolerância é aplicada depois, quando as páginas pareadas são comparadas, então páginas de só ruído ainda terminam como unchanged
Consigo uma imagem de overlay para cada página de um documento?
Não pela chamada de documento, que omite máscaras e overlays de propósito para manter a memória plana. Use o JSON para achar as páginas alteradas, depois chame CompareRenderedPagesWith com IncludeOverlay nessas páginas
A comparação visual, o pareamento de páginas e o resto das APIs de renderização e de documentos vêm com a biblioteca PDF PDFlibPas para Delphi