Articol tehnic

Compară două fișiere PDF în Delphi: structură și pixeli

HotPDF compară două documente PDF din Delphi prin THPDFDocComparison, care parcurge graful de obiecte al ambelor fișiere pornind de la catalog și, la cerere, randează și fiecare pereche de pagini, măsurând pixelii care diferă. Rezultatul este un raport JSON care numește fiecare diferență găsită, bugetul consumat și dacă comparația a rulat până la final. Ambele treceri contează, deoarece o comparație structurală și una vizuală răspund la întrebări diferite

Întrebarea din spatele funcției este de obicei o întrebare de release. Un motor de rapoarte primește o modificare, rezultatul este regenerat, iar cineva trebuie să decidă dacă s-a schimbat ceva. Deschiderea ambelor fișiere alăturat funcționează până la vreo trei pagini, apoi atenția cedează. Compararea octeților bruți eșuează instant, deoarece două rulări ale aceluiași generator produc octeți diferiți din motive care nu au nicio legătură cu ce vede un cititor

De ce pot fi două PDF-uri diferite ca octeți, dar identice vizual?

Două PDF-uri generate independent care se tipăresc identic diferă frecvent la nivel de octeți, iar motivele sunt structurale, nu cosmetice. Numerele de obiect sunt atribuite în ordinea în care obiectele se întâmplă să fie scrise. Subseturile de fonturi alocă CID-uri în ordinea în care glifele sunt întâlnite prima dată, așa că un subset construit în urma unei parcurgeri ușor diferite produce octeți de flux de conținut diferiți pentru același text vizibil. Offseturile de referințe încrucișate se deplasează de fiecare dată când ceva din amonte își schimbă lungimea

De aceea numerele de obiect nu pot fi folosite ca identitate între documente. HotPDF construiește în schimb fiecare instantaneu parcurgând de la catalog, extinzând dicționarele în ordinea octeților cheilor lor și array-urile după index, astfel încât fiecare obiect este numit prin calea care ajunge la el. Obiectele pe care parcurgerea nu le poate atinge de la rădăcină revin la o cale sintetică $Unreachable[...] care poartă numărul de obiect și generația, ceea ce păstrează conținutul orfan vizibil în raport, nu absent în tăcere

Fluxurile nu sunt comparate prin copiere. Fiecare flux contribuie cu o semnătură SHA-256 incrementală, calculată în timp ce se restaurează ulterior poziția originală a fluxului, astfel încât compararea a două fișiere de sute de megaocteți nu înseamnă materializarea a sute de megaocteți de două ori

Alinierea paginilor când un document are o inserție

Compararea paginii 1 cu pagina 1, a paginii 2 cu pagina 2 și așa mai departe este corectă doar când nu s-a inserat nimic. Inserează o pagină de copertă, iar o comparație naivă raportează fiecare pagină ca schimbată, ceea ce este tehnic adevărat și practic inutil

HotPDF aliniază paginile înainte de a le compara. Construiește o semnătură per pagină din textul extractibil, revine la o semnătură structurală pentru paginile fără text, apoi calculează cea mai lungă subsecvență crescătoare peste indicii țintă potriviți. Paginile din interiorul acelei subsecvențe sunt cele care doar s-au deplasat; paginile din afara ei sunt mutări reale. Această distincție este cea care face lizibilă o comparație a unui manual de 400 de pagini, deoarece raportul spune că o pagină a fost inserată, nu că patru sute de pagini s-au schimbat

Rularea unei comparații structurale

Cel mai simplu apel primește două documente încărcate și un mod. cmStructural execută parcurgerea grafului de obiecte, cmRenderedImage execută comparația de pixeli, cmFull face ambele, iar modurile mai ușoare cmPageCount, cmPageText și cmObjectCount există pentru verificări rapide și ieftine:

uses
  HPDFDoc, HPDFDocCompare;

var
  DocA, DocB: THotPDF;
  Report: AnsiString;
begin
  DocA := THotPDF.Create(nil);
  DocB := THotPDF.Create(nil);
  try
    if (DocA.LoadFromFile('baseline.pdf') <= 0) or
       (DocB.LoadFromFile('candidate.pdf') <= 0) then
      Exit;
    Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
    with TFileStream.Create('diff.json', fmCreate) do
    try
      WriteBuffer(Report[1], Length(Report));
    finally
      Free;
    end;
  finally
    DocB.Free;
    DocA.Free;
  end;
end;

Raportul distinge trei stări pe care un boolean nu le poate exprima. identical spune dacă ceva a diferit, comparisonComplete spune dacă parcurgerea s-a terminat, iar comparisonBudget numește limita care a oprit-o, dacă a fost cazul. O comparație care epuizează un buget raportează comparisonComplete=false și identical=false împreună, deoarece o parcurgere trunchiată nu are niciun temei să afirme egalitate. Orice automatizare care citește doar identical va ajunge, la un moment dat, să trateze o oprire de buget ca pe o diferență reală, așa că citește toate trei

Ce limite mențin parcurgerea mărginită?

Valorile implicite din THPDFStructuralCompareLimits.Default sunt dimensionate pentru documente reale, nu pentru unele adversariale, iar fiecare buget relevant semantic are propriul plafon: 250.000 de obiecte, 2.000.000 de muchii, adâncime 128, 10.000 de diferențe raportate, 64 MB per flux și 512 MB de octeți de flux în total, 1 MB per valoare și 4.096 de octeți per cale. Ridică-le deliberat când îți cunoști corpusul, și coboară-le când compari fișiere venite din afară:

var
  Limits: THPDFStructuralCompareLimits;
  Options: THPDFRenderedCompareOptions;
begin
  Limits := THPDFStructuralCompareLimits.Default;
  Limits.MaxDifferences := 200;        // eșuează rapid în CI
  Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;

  Options := THPDFRenderedCompareOptions.Default;
  Options.DPI := 150;                  // implicit este 72
  Options.ColorTolerance := 2;         // ignoră zgomotul de rotunjire de 1-2 niveluri
  Options.MinimumSimilarity := 0.9995;
  Options.MaxChangedPixelRatio := 0.0005;
  Options.GenerateHeatmaps := True;    // scrie imagini de suprapunere pentru revizuire

  Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
    Limits, Options);
end;

Trecerea de randare estimează numărul de pixeli din dimensiunile paginii și DPI-ul cerut înainte ca vreun bitmap să fie alocat, apoi reverifică bitmap-ul real după aceea, astfel încât o geometrie de pagină malformată nu poate trece de buget mințind despre dimensiunea sa. Creșterea DPI ridică fidelitatea și costul pătratic: 150 DPI înseamnă de patru ori mai mulți pixeli decât 72, iar plafoanele per pagină și totale există exact pentru că un job pe loturi la 300 DPI ar aloca altfel resurse până în probleme

Cât de similar înseamnă suficient de similar?

Două pagini se consideră similare doar când ambele condiții sunt îndeplinite: raportul de pixeli schimbați este la sau sub MaxChangedPixelRatio, iar similaritatea este la sau peste MinimumSimilarity. Două praguri în loc de unul, deoarece câțiva pixeli catastrofal de greșiți și un val larg de schimbări minore de culoare sunt eșecuri diferite, iar oricare dintre ele poate fi acceptabil într-un flux de lucru și descalificant în altul. Testele de prag folosesc valori nerotunjite; cele șase zecimale din JSON există pentru a păstra rapoartele stabile și comparabile prin diff, nu pentru a defini comparația

Pixelii schimbați sunt grupați în regiuni folosind dale de dimensiune fixă ca noduri cu adiacență în patru direcții, nu prin flood fill per pixel. Asta menține memoria mărginită și lista de regiuni stabilă între rulări. Trunchierea detaliului de regiuni reținute afectează doar listarea, nu numărul de regiuni raportat, așa că o pagină cu mai multe regiuni schimbate decât MaxChangedRegions tot raportează câte au fost

Un comportament merită menționat explicit, deoarece inversează instinctul obișnuit. Eșecurile de randare, eșecurile de alocare și eșecurile de suprapunere nu sunt niciodată înghițite. Orice de acest fel este înregistrat ca renderError sau renderBudget și forțează renderComparisonComplete=false, deoarece o pagină care nu a reușit să fie randată este o pagină pe care nimeni nu a comparat-o, iar raportarea ei ca identică este mai rea decât să nu raportezi nimic

Unde se potrivește fiecare mod într-un pipeline

Comparația structurală răspunde ce s-a schimbat și este alegerea implicită corectă pentru suitele de regresie: numește calea, indexul paginii și numerele de obiect implicate, astfel încât un eșec indică direct codul care l-a produs. Comparația randată răspunde dacă cineva va observa, care este întrebarea potrivită pentru aprobări și pentru verificarea că o optimizare a fost cu adevărat fără pierderi

Se combină bine. Rulează cmStructural la fiecare build și lasă-l să eșueze zgomotos la schimbări neașteptate la nivel de obiect; rulează cmFull cu hărți termice înainte de un release, când e disponibil un om să se uite peste suprapuneri. Pentru pipeline-uri care emit deja marcaj de pagină din alte motive, rezultatul text descris în exportul paginilor PDF în SVG oferă o a treia vizualizare, comparabilă manual, iar verificările automate din automatizarea rapoartelor de preflight acoperă întrebări de conformitate la care niciun mod de comparație nu este menit să răspundă

Comparația, preflight-ul și randarea împart același model de obiect al documentului încărcat, așa că o singură trecere peste un fișier poate alimenta toate trei. Lista completă de funcții pentru Delphi și C++Builder este pe pagina componentei HotPDF PDF pentru Delphi