Το HotPDF συγκρίνει δύο έγγραφα PDF από τη Delphi μέσω της THPDFDocComparison, η οποία διασχίζει το γράφημα αντικειμένων και των δύο αρχείων από τον κατάλογο προς τα έξω και, όταν ζητηθεί, αποδίδει επίσης κάθε ζεύγος σελίδων και μετρά τα pixel που διαφέρουν. Το αποτέλεσμα είναι μια αναφορά JSON που ονομάζει κάθε διαφορά που βρέθηκε, το όριο που καταναλώθηκε, και αν η σύγκριση ολοκληρώθηκε πλήρως. Και τα δύο περάσματα έχουν σημασία, επειδή μια δομική σύγκριση και μια οπτική σύγκριση απαντούν σε διαφορετικές ερωτήσεις
Το ερώτημα πίσω από το χαρακτηριστικό είναι συνήθως ζήτημα έκδοσης. Μια μηχανή αναφορών λαμβάνει μια αλλαγή, η έξοδος αναδημιουργείται, και κάποιος πρέπει να αποφασίσει αν κάτι μετακινήθηκε. Το άνοιγμα και των δύο αρχείων δίπλα-δίπλα κλιμακώνεται μέχρι περίπου τρεις σελίδες πριν η προσοχή αποτύχει. Η σύγκριση ακατέργαστων byte αποτυγχάνει αμέσως, αφού δύο εκτελέσεις της ίδιας γεννήτριας παράγουν διαφορετικά byte για λόγους που δεν έχουν καμία σχέση με το τι βλέπει ένας αναγνώστης
Γιατί τα PDF μπορούν να διαφέρουν σε byte αλλά να είναι οπτικά πανομοιότυπα;
Δύο ανεξάρτητα δημιουργημένα PDF που εκτυπώνονται πανομοιότυπα διαφέρουν συχνά στα byte τους, και οι λόγοι είναι δομικοί και όχι αισθητικοί. Οι αριθμοί αντικειμένων εκχωρούνται με τη σειρά που τυχαίνει να γραφούν τα αντικείμενα. Τα υποσύνολα γραμματοσειρών εκχωρούν CID με τη σειρά που συναντώνται πρώτα οι γλυφές, οπότε ένα υποσύνολο που χτίστηκε κατά τη διάρκεια ελαφρώς διαφορετικής διάσχισης παράγει διαφορετικά byte ροής περιεχομένου για το ίδιο ορατό κείμενο. Οι μετατοπίσεις παραπομπών παύλας μετατοπίζονται όποτε αλλάζει το μήκος οτιδήποτε προηγείται
Γι' αυτό οι αριθμοί αντικειμένων δεν μπορούν να χρησιμοποιηθούν ως ταυτότητα μεταξύ εγγράφων. Το HotPDF αντ' αυτού χτίζει κάθε στιγμιότυπο διασχίζοντας από τον κατάλογο, επεκτείνοντας λεξικά με τη σειρά byte των κλειδιών τους και πίνακες με ευρετήριο, ώστε κάθε αντικείμενο να ονομάζεται από τη διαδρομή που το φτάνει. Αντικείμενα που η διάσχιση δεν μπορεί να φτάσει από τη ρίζα επιστρέφουν σε μια συνθετική διαδρομή $Unreachable[...] που φέρει τον αριθμό αντικειμένου και τη γενιά, κάτι που κρατά ορατό σε αναφορά το ορφανό περιεχόμενο αντί να το αποσιωπά
Οι ροές δεν συγκρίνονται με αντιγραφή. Κάθε ροή συνεισφέρει μια αυξητική υπογραφή SHA-256, υπολογισμένη ενώ αποκαθίσταται στη συνέχεια η αρχική θέση ροής, ώστε η σύγκριση δύο αρχείων εκατοντάδων megabyte να μη σημαίνει την υλοποίηση διακοσίων megabyte δύο φορές
Ευθυγράμμιση σελίδων όταν ένα έγγραφο έχει εισαγωγή
Η σύγκριση της σελίδας 1 με τη σελίδα 1, της σελίδας 2 με τη σελίδα 2 και ούτω καθεξής είναι σωστή μόνο όταν δεν έχει εισαχθεί τίποτα. Εισάγετε μια σελίδα εξωφύλλου και μια απλοϊκή σύγκριση αναφέρει κάθε σελίδα ως αλλαγμένη, κάτι που είναι τεχνικά σωστό αλλά επιχειρησιακά άχρηστο
Το HotPDF ευθυγραμμίζει τις σελίδες πριν τις συγκρίνει. Χτίζει μια υπογραφή ανά σελίδα από το εξαγώγιμο κείμενο, επιστρέφει σε μια δομική υπογραφή για σελίδες χωρίς κείμενο, και έπειτα υπολογίζει τη μακρύτερη αύξουσα υποακολουθία στα αντιστοιχισμένα ευρετήρια-στόχους. Οι σελίδες μέσα σε αυτή την υποακολουθία είναι απλώς εκείνες που μετατοπίστηκαν· οι σελίδες εκτός της είναι γνήσιες μετακινήσεις. Η διάκριση είναι αυτό που κάνει αναγνώσιμη μια σύγκριση ενός εγχειριδίου 400 σελίδων, επειδή η αναφορά λέει ότι εισήχθη μία σελίδα αντί να λέει ότι άλλαξαν τετρακόσιες σελίδες
Εκτέλεση δομικής σύγκρισης
Η απλούστερη κλήση δέχεται δύο φορτωμένα έγγραφα και μια λειτουργία. Η cmStructural εκτελεί τη διάσχιση του γραφήματος αντικειμένων, η cmRenderedImage εκτελεί τη σύγκριση pixel, η cmFull κάνει και τα δύο, και οι ελαφρύτερες λειτουργίες cmPageCount, cmPageText και cmObjectCount υπάρχουν για φθηνούς ελέγχους καπνού:
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;
Η αναφορά διακρίνει τρεις καταστάσεις που ένα boolean δεν μπορεί. Το identical λέει αν κάτι διέφερε, το comparisonComplete λέει αν η διάσχιση ολοκληρώθηκε, και το comparisonBudget ονομάζει το όριο που τη σταμάτησε, αν κάποιο το σταμάτησε. Μια σύγκριση που εξαντλεί ένα όριο αναφέρει μαζί comparisonComplete=false και identical=false, επειδή μια περικομμένη διάσχιση δεν έχει βάση για να ισχυριστεί ισότητα. Οποιαδήποτε αυτοματοποίηση διαβάζει μόνο το identical τελικά θα αντιμετωπίσει μια διακοπή ορίου ως πραγματική διαφορά, οπότε διαβάστε και τα τρία
Ποια όρια κρατούν τη διάσχιση φραγμένη;
Οι προεπιλογές στο THPDFStructuralCompareLimits.Default έχουν διαστασιολογηθεί για πραγματικά έγγραφα και όχι για εχθρικά, και κάθε σημασιολογικά σχετικό όριο έχει το δικό του ανώτατο όριο: 250.000 αντικείμενα, 2.000.000 ακμές, βάθος 128, 10.000 αναφερόμενες διαφορές, 64 MB ανά ροή και 512 MB συνολικών bytes ροής, 1 MB ανά τιμή και 4.096 bytes ανά διαδρομή. Αυξήστε τα σκόπιμα όταν γνωρίζετε το σύνολο εγγράφων σας, και μειώστε τα όταν συγκρίνετε αρχεία που έφτασαν από έξω:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // αποτυχία γρήγορα στο CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // η προεπιλογή είναι 72
Options.ColorTolerance := 2; // αγνόηση θορύβου στρογγυλοποίησης 1-2 επιπέδων
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // εγγραφή εικόνων επικάλυψης για ανασκόπηση
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Το πέρασμα απόδοσης εκτιμά τον αριθμό pixel από τις διαστάσεις σελίδας και τα ζητούμενα DPI πριν εκχωρηθεί οποιοδήποτε bitmap, και επανελέγχει το πραγματικό bitmap στη συνέχεια, ώστε μια παραμορφωμένη γεωμετρία σελίδας να μην μπορεί να ξεγλιστρήσει από το όριο ψευδόμενη για το μέγεθός της. Η αύξηση των DPI αυξάνει την πιστότητα και το κόστος τετραγωνικά: τα 150 DPI είναι τετραπλάσια pixel των 72, και τα ανώτατα όρια pixel ανά σελίδα και συνολικά υπάρχουν ακριβώς επειδή μια παρτίδα εργασιών στα 300 DPI θα εκχωρήσει διαφορετικά τον δρόμο της προς τον μπελά
Πόσο όμοιο είναι αρκετά όμοιο;
Δύο σελίδες μετράνε ως όμοιες μόνο όταν ισχύουν και οι δύο συνθήκες: ο λόγος αλλαγμένων pixel είναι ίσος ή κάτω από το MaxChangedPixelRatio και η ομοιότητα είναι ίση ή πάνω από το MinimumSimilarity. Δύο κατώφλια αντί για ένα, επειδή μια χούφτα καταστροφικά λανθασμένων pixel και ένα ευρύ κύμα μικρών χρωματικών μετατοπίσεων είναι διαφορετικές αποτυχίες, και οποιαδήποτε από τις δύο μπορεί να είναι αποδεκτή σε μια ροή εργασίας και ασυγχώρητη σε άλλη. Οι δοκιμές κατωφλίου χρησιμοποιούν μη στρογγυλοποιημένες τιμές· τα έξι δεκαδικά ψηφία στο JSON υπάρχουν για να κρατούν τις αναφορές σταθερές και συγκρίσιμες, όχι για να ορίζουν τη σύγκριση
Τα αλλαγμένα pixel ομαδοποιούνται σε περιοχές χρησιμοποιώντας πλακίδια σταθερού μεγέθους ως κόμβους με τετράπλευρη γειτνίαση, αντί για πλημμύρα ανά pixel. Αυτό κρατά τη μνήμη φραγμένη και τη λίστα περιοχών σταθερή μεταξύ εκτελέσεων. Η περικοπή της διατηρούμενης λεπτομέρειας περιοχής επηρεάζει μόνο τη λίστα, όχι τον αναφερόμενο αριθμό περιοχών, οπότε μια σελίδα με περισσότερες αλλαγμένες περιοχές από το MaxChangedRegions εξακολουθεί να αναφέρει πόσες υπήρχαν
Μια συμπεριφορά αξίζει να δηλωθεί ξεκάθαρα επειδή αντιστρέφει το συνηθισμένο ένστικτο. Αποτυχίες αποδότη, αποτυχίες εκχώρησης και αποτυχίες επικάλυψης δεν καταπίνονται ποτέ. Οτιδήποτε τέτοιο καταγράφεται ως renderError ή renderBudget και εξαναγκάζει renderComparisonComplete=false, επειδή μια σελίδα που απέτυχε να αποδοθεί είναι μια σελίδα που κανείς δεν συνέκρινε, και η αναφορά της ως πανομοιότυπης είναι χειρότερη από το να μην αναφερθεί καθόλου
Πού ανήκει κάθε λειτουργία σε έναν αγωγό
Η δομική σύγκριση απαντά τι άλλαξε και είναι η σωστή προεπιλογή για σουίτες οπισθοδρόμησης: ονομάζει τη διαδρομή, το ευρετήριο σελίδας και τους αριθμούς αντικειμένων που εμπλέκονται, ώστε μια αποτυχία να δείχνει στον κώδικα που την παρήγαγε. Η αποδοσμένη σύγκριση απαντά αν κάποιος θα το προσέξει, που είναι η ερώτηση για εγκρίσεις και για επαλήθευση ότι ένα πέρασμα βελτιστοποίησης ήταν πράγματι χωρίς απώλειες
Συνδυάζονται καλά. Εκτελέστε την cmStructural σε κάθε έκδοση και αφήστε την να αποτύχει έντονα σε απρόσμενες αλλαγές σε επίπεδο αντικειμένου· εκτελέστε την cmFull με χάρτες θερμότητας πριν από μια κυκλοφορία, όταν υπάρχει διαθέσιμος άνθρωπος να δει τις επικαλύψεις. Για αγωγούς που ήδη εκπέμπουν σήμανση σελίδας για άλλους λόγους, η έξοδος κειμένου που περιγράφεται στην εξαγωγή σελίδων PDF σε SVG δίνει μια τρίτη, αναγνώσιμη από ανθρώπους προβολή, και οι αυτοματοποιημένοι έλεγχοι στην αυτοματοποίηση αναφορών preflight καλύπτουν ερωτήματα συμμόρφωσης που καμία από τις δύο λειτουργίες σύγκρισης δεν προορίζεται να απαντήσει
Η σύγκριση, το preflight και η απόδοση μοιράζονται το ίδιο μοντέλο αντικειμένων φορτωμένου εγγράφου, οπότε ένα μόνο πέρασμα πάνω σε ένα αρχείο μπορεί να τροφοδοτήσει και τα τρία. Η πλήρης λίστα χαρακτηριστικών για Delphi και C++Builder βρίσκεται στη σελίδα του εξαρτήματος HotPDF PDF για Delphi