La comparaison visuelle de PDFlibPas rend deux PDFs au même DPI et fait le diff de ce qu'un lecteur voit réellement. TPDFlib.CompareRenderedPagesWith compare une paire de pages et renvoie les pixels modifiés, des régions, un masque et un overlay. TPDFlib.CompareDocumentVisualsWith et TPDFlib.CompareFileVisuals apparient des documents entiers page par page, détectent les pages insérées et supprimées, et rapportent tout en JSON
Le cas d'usage est le test de régression visuelle. Quelqu'un réécrit le modèle de facture, met à jour un paquet de polices ou change de moteur de rendu, et la question n'est pas de savoir si les octets ont changé (ils changent toujours) mais si un client verra une différence. Un diff structurel répond à une autre question. Deux fichiers peuvent différer dans chaque flux de contenu et peindre des pages identiques, et un opérande décalé peut bouger un total de trois points sans toucher quoi que ce soit qu'un extracteur de texte signalerait. Les pixels sont la seule couche où "semble identique" soit testable
Que compare réellement un diff visuel de PDF ?
PDFlibPas compare le résultat visible sur une page blanche, pas les contenus bruts des buffers. Chaque page est rendue via RenderPageToString en BMP non compressé, décodée en bitmap top-down, et tout pixel porteur d'alpha est composité sur blanc avant la comparaison des canaux. Un bitmap RGB et un bitmap RGBA identiques à l'écran se comparent donc comme identiques, exactement ce qu'on veut quand les deux côtés viennent de chemins de rendu différents
Le décodeur BMP a une bizarrerie délibérée à connaître. Un bitmap 32 bits BI_RGB porte un quatrième octet par pixel, mais le format laisse cet octet indéfini : certains générateurs le remplissent avec 0, d'autres avec 255, d'autres avec des restes. PLDecodeVisualBMP l'ignore et garde la couleur. Traiter cet octet comme de l'opacité permettrait à un moteur qui écrit des zéros d'effacer chaque pixel de la page, et le diff rapporterait gentiment deux documents vides et identiques. Si vous fournissez vos propres bitmaps à l'API de plus bas niveau, l'alpha n'est honoré que si vous déclarez vous-même le format comme vpfRGBA ou vpfBGRA
Un pixel compte comme modifié dès qu'un de ses canaux rouge, vert ou bleu diffère de plus de ColorTolerance. Une tolérance 0 signifie l'égalité bit à bit. En pratique, une valeur à un chiffre absorbe le frémissement d'anticrénelage entre builds du moteur sans masquer un glyphe modifié, car un vrai changement de texte déplace les canaux bien plus que cela. Choisissez le nombre depuis une exécution de référence connue comme bonne, pas en devinant
Comparer une seule paire de pages avec CompareRenderedPagesWith
TPDFlib.CompareRenderedPagesWith est l'outil quand vous savez déjà quelles pages vont ensemble et voulez du détail : une bounding box, des régions de modification connexes, un masque par pixel optionnel et un overlay teinté en rouge optionnel. L'enregistrement d'options et les types de résultat vivent dans l'unité PDFlibVisualCompare, et PLDefaultVisualCompareOptions vous donne tolérance 0, tuiles de 32 pixels, aucune région ignorée, ni masque ni 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;
// Masquez l'horodatage d'impression ; unités en pixels au DPI ci-dessous
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;
Trois détails mordent ici. Les rectangles ignorés sont en pixels rendus avec une origine en haut à gauche et des intervalles semi-ouverts, donc Right et Bottom sont exclusifs, et le même rectangle couvre une autre partie de la page si vous changez le DPI. Les rectangles ignorés qui se chevauchent comptent une fois, et les pixels ignorés apparaissent dans IgnoredPixels au lieu de disparaître, ce qui garde ChangedRatio honnête parce qu'il divise par les seuls pixels comparés. Enfin, l'appel mono-page exige que les deux rendus aient les mêmes dimensions ; une page qui a changé de taille échoue avec une erreur au lieu d'être étirée pour rentrer
Les régions viennent d'une passe par tuiles : les pixels modifiés sont rangés dans des carrés de TileSize et les tuiles adjacentes sont fusionnées en une région avec ses propres limites et son compte de pixels. Des tuiles plus grandes donnent moins de régions, plus grossières, ce qui est en général ce que veut un rapport de test. L'overlay est une copie RGB de la seconde page avec les pixels modifiés teintés en rouge, renvoyée en octets bruts dans Comparison.Overlay ; l'écrire en PNG pour vos artefacts de CI, c'est votre code, pas celui de la bibliothèque
Comment l'appariement de pages au niveau document gère-t-il les pages insérées ?
TPDFlib.CompareDocumentVisualsWith apparie les pages avec une plus longue sous-séquence commune sur des empreintes de pixels exactes, pour qu'une page insérée ne transforme pas chaque page suivante en fausse modification. Une boucle naïve page 1 vers page 1, page 2 vers page 2 est inutile dès qu'un modèle gagne une page de couverture : chaque page après l'insertion est décalée d'un cran et le rapport s'allume en rouge de haut en bas
Chaque page est rendue et réduite à une clé faite de sa largeur en pixels, sa hauteur et un condensé SHA-256 des pixels visibles. La taille compte plus qu'il n'y paraît : une page portrait tout blanc et une page paysage tout blanc de même aire ont des buffers de pixels identiques octet pour octet, et sans les dimensions dans la clé elles s'ancreraient l'une à l'autre. Les clés identiques des deux côtés deviennent des ancres, le LCS choisit la plus longue chaîne ordonnée d'entre elles, et les pages entre deux ancres consécutives sont appariées dans l'ordre. Quand un écart contient plus de pages d'un côté, le surplus est rapporté comme added ou removed
var
Options: TPDFVisualCompareOptions;
Report: WideString;
begin
// Baseline et Candidate chargés comme avant, avec des réglages de rendu identiques
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;
Les ancres sont exactes au bit près, et c'est un choix de conception, pas un oubli. ColorTolerance ne s'applique qu'à la comparaison des pages appariées, jamais à l'empreinte. Une page qui n'a que du bruit d'anticrénelage ne s'ancrera pas, mais elle tombe quand même dans un écart, se fait apparier dans l'ordre avec sa vis-à-vis, puis se compare comme unchanged sous tolérance. Des ancres floues seraient plus tolérantes et bien moins prévisibles, parce que des pages quasi dupliquées (séparateurs vierges, pages de conditions répétées) commenceraient à s'apparier entre elles
Soyez clair sur ce qu'est cet alignement. Le JSON le nomme exact-raster-lcs-gap-order exprès : c'est un alignement raster, pas une reconnaissance sémantique des pages. Si un écart contient une page réécrite et une page insérée, l'appariement dans l'ordre peut comparer la réécriture contre l'insertion et rapporter l'originale comme supprimée. Les documents pleins de pages identiques sont pareillement ambigus. Quand l'appariement importe pour un verdict, regardez les paires, pas seulement les compteurs
Lire le rapport JSON
Le rapport documentaire a une forme fixe sur laquelle vous pouvez faire des assertions dans un runner de test. En cas de succès, il porte "status":"complete", "complete":true, le nom d'alignement, firstPageCount, secondPageCount, et les compteurs changedPages, addedPages, removedPages et unchangedPages. Le tableau pairs liste ensuite chaque ligne avec firstPage, secondPage (0 pour le côté manquant) et un kind parmi unchanged, changed, size_changed, added ou removed
Les lignes de type changed et unchanged embarquent un objet comparison avec width, height, comparedPixels, changedPixels, ignoredPixels, changedRatio, boundingBox (null quand rien n'a changé) et une liste regions de bounds plus changedPixels. Une ligne size_changed n'a pas de comparaison du tout ; elle rapporte firstWidth, firstHeight, secondWidth et secondHeight et compte dans changedPages. Étirer une page redimensionnée pour la comparer produirait un mur de pixels modifiés sans signification, donc la bibliothèque rapporte le changement de géométrie et s'arrête là
Le rapport documentaire ne contient jamais de masques ni d'overlays. CompareDocumentVisualsWith force IncludeMask et IncludeOverlay à off même si vos options le demandent, pour que la mémoire reste plate quand le nombre de pages grandit au lieu de garder un aperçu pleine page pour chaque paire. Servez-vous du rapport pour trouver les pages intéressantes, puis appelez CompareRenderedPagesWith sur ces seules pages pour obtenir l'overlay. Les échecs reviennent en "status":"error", "complete":false avec une chaîne error, et LastErrorCode vaut 401
Comparer contre un fichier avec CompareFileVisuals
TPDFlib.CompareFileVisuals est la forme en un appel pour la CI : il charge l'autre PDF, copie dessus les réglages de rendu courants, et renvoie le même JSON documentaire. La signature est CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance), où la tolérance doit aller de 0 à 255 et le DPI doit être fini et positif
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;
L'héritage du moteur de rendu est la raison de préférer cet appel au chargement du second fichier vous-même. Le document chargé prend l'échelle de rendu, le type de crop et les réglages graphiques de l'instance courante, plus le choix de moteur Cairo ou PDFium quand ceux-ci sont compilés. Si vous construisez deux instances TPDFlib à la main pour CompareDocumentVisualsWith, les configurer toutes deux à l'identique est votre affaire, et un décalage apparaît comme toutes les pages modifiées. L'appel ne peut pas non plus prendre de régions ignorées ; tournez-vous vers l'API typée quand vous en avez besoin
Limites, coût et place du diff structurel
PDFlibPas borne le travail explicitement pour qu'une entrée hostile ou énorme échoue avec une erreur au lieu d'épuiser la mémoire. Une page rendue ne peut pas dépasser 64 Mi pixels, la grille de tuiles est plafonnée à 4 Mi tuiles, et l'alignement de pages refuse les entrées où la somme des deux comptes de pages dépasse 8192 ou où la matrice LCS excéderait 4 Mi cellules. La comparaison documentaire rend aussi chaque page deux fois, une fois pour l'empreinte et une autre pour la comparaison appariée ; budgétez cela à 150 DPI et au-delà. Pour la plupart des suites de régression, 72 à 96 DPI attrape les changements de mise en page et de texte pour une fraction du temps
Diff visuel et diff structurel répondent à des questions différentes, et un pipeline sérieux veut en général les deux. La comparaison pixel dit si un lecteur remarquerait quelque chose. Elle ne peut pas dire si un document signé a gagné une annotation qui se rend hors page, ni si une mise à jour incrémentale a remplacé en silence la valeur d'un champ de formulaire par le même texte visible. Pour cette couche, classifier ce qui a changé dans un PDF après signature parcourt le diff au niveau des révisions. Et parce que le choix du moteur pilote les pixels, le rendu PDF multi-moteurs en Delphi se lit avant de figer une référence sur un moteur
FAQ
La comparaison visuelle de PDFlibPas étire-t-elle les pages qui ont changé de taille ?
Non. CompareRenderedPagesWith renvoie une erreur pour des pages de taille pixel différente, et le rapport documentaire marque ces paires size_changed avec les deux dimensions au lieu de les comparer
Pourquoi les pages quasi identiques ne servent-elles pas d'ancres ?
Les ancres utilisent une empreinte SHA-256 exacte des pixels visibles plus la taille de page, pour que l'alignement reste prévisible. La tolérance s'applique ensuite, quand les pages appariées sont comparées, si bien que les pages de pur bruit finissent quand même unchanged
Peut-on obtenir une image overlay pour chaque page d'un document ?
Pas via l'appel documentaire, qui omet délibérément masques et overlays pour garder une mémoire plate. Utilisez le JSON pour trouver les pages modifiées, puis appelez CompareRenderedPagesWith avec IncludeOverlay sur ces pages
La comparaison visuelle, l'appariement de pages et le reste des API de rendu et de document sont livrés avec la bibliothèque PDF PDFlibPas pour Delphi