Ένα εργαλείο preflight δέσμης (batch preflight) είναι ένα πρόγραμμα κονσόλας χωρίς παράθυρο, που στοχεύει σε έναν φάκελο με PDF, τα οποία επικυρώνει το καθένα βάσει των προτύπων συμμόρφωσης που ορίζετε, και αφήνει πίσω του αναγνώσιμη από μηχανήματα απόδειξη του τι βρήκε. Κανείς δεν κάθεται να το παρακολουθήσει. Λειτουργεί στις δύο το πρωί μέσω cron ή του Χρονοδιακόπτη Εργασιών των Windows (Task Scheduler), ή ως πύλη ελέγχου (gate) σε μια διοχέτευση (pipeline) CI, και το επόμενο άτομο που θα νοιαστεί για την έξοδό του είναι είτε ένας χρονοπρογραμματιστής (scheduler) που διαβάζει έναν κωδικό εξόδου είτε ένας ελεγκτής που ανοίγει μια αναφορά εβδομάδες αργότερα. Αυτό αλλάζει το τι σημαίνει "σωστό". Ο κινητήρας preflight του PDFium Component, μιας βιβλιοθήκης πηγαίου κώδικα PDF για Delphi, C++Builder και Lazarus, καθιστά τις ίδιες τις κλήσεις επικύρωσης σχεδόν ασήμαντες. Η δουλειά που αποφασίζει αν το εργαλείο αξίζει τα χρήματά του βρίσκεται γύρω από αυτές τις κλήσεις: ποιο προφίλ ελέγξατε, τι είπε ο κωδικός εξόδου στον χρονοπρογραμματιστή, και αν η αναφορά που θα είχε πιάσει ένα λάθος εξακολουθεί να υπάρχει όταν κάποιος την αναζητήσει
Η σύμβαση: τι μπορεί πραγματικά να δει ένας χρονοπρογραμματιστής (scheduler)
Ένας εκτελεστής CI (CI runner) ή ο Χρονοδιακόπτης Εργασιών των Windows βλέπει ακριβώς δύο πράγματα από το εργαλείο σας: τον κωδικό εξόδου (exit code) και τα αρχεία που άφησε πίσω του. Γραμμές καταγραφής (log lines), χρώματα κονσόλας, έξοδος προόδου: όλα αυτά είναι για έναν άνθρωπο που παρακολουθεί ζωντανά, και στις δύο το πρωί κανείς δεν το κάνει. Επομένως, διορθώστε το λεξιλόγιο του κωδικού εξόδου πριν αγγίξετε το API και κρατήστε το βαρετό:
0: κάθε αρχείο συμμορφώθηκε με κάθε αιτούμενο προφίλ1: τουλάχιστον ένα αρχείο παρήγαγε ευρήματα επικύρωσης2: το ίδιο το εργαλείο απέτυχε σε τουλάχιστον ένα αρχείο (κατεστραμμένη είσοδος, κλείδωμα, κατάρρευση)
Η διάκριση μεταξύ των κωδικών 1 και 2 είναι αυτή που οι ομάδες παραλείπουν και αργότερα μετανιώνουν. Ένα κατεστραμμένο PDF που δεν ανοίγει δεν είναι αποτυχία επικύρωσης. Αν το εντάξετε στον κωδικό 1, τότε ένα φορτίο κατεστραμμένων σαρώσεων θα εμφανιστεί στους πίνακες εργαλείων σας ως ξαφνική κατάρρευση συμμόρφωσης, στέλνοντας κάποιον να κυνηγήσει μια υποβάθμιση (regression) προτύπων που δεν συνέβη ποτέ, όταν η πραγματική ιστορία είναι ένας χαλασμένος σαρωτής στο ανάντη (upstream)
Δύο ακόμη στοιχεία ανήκουν στη σύμβαση. Το πρώτο είναι ένα χρονικό όριο (timeout) ανά αρχείο. Ένα παθολογικό PDF, με χιλιάδες σελίδες και βαθιά ένθετες δομές αντικειμένων, μπορεί να κρατήσει ένα μόνο πέρασμα επικύρωσης για λεπτά, και ένα νυχτερινό παράθυρο (nightly window) δεν έχει υπομονή για αυτό. Τερματίστε την εργασία αυτού του αρχείου στην προθεσμία, μετρήστε το ως αποτυχία του εργαλείου και συνεχίστε τη δέσμη. Το δεύτερο είναι ένας κατάλογος καραντίνας (quarantine): μετακινήστε κάθε είσοδο που έχει λήξει το χρονικό της όριο ή δεν μπορεί να ανοιχτεί στην άκρη αντί να την αφήσετε στη θέση της. Μέσα σε λίγους μήνες αυτός ο κατάλογος συσσωρεύει αθόρυβα τα χειρότερα έγγραφα που στέλνουν οι πραγματικοί πελάτες σας, και αυτό το σώμα (corpus) αξίζει περισσότερο για τις δοκιμές κυκλοφορίας από οποιοδήποτε συνθετικό δείγμα θα μπορούσατε να γράψετε με το χέρι
Επιλογή προτύπων, και γιατί έχει σημασία το επίπεδο συμμόρφωσης
Η απαρίθμηση (enumeration) TPdfPreflightStandard καλύπτει τις οικογένειες που εμφανίζονται στην πράξη: ppsPdfA για αρχειακή συμμόρφωση ISO 19005, ppsPdfUa για προσβασιμότητα ISO 14289, ppsPdfX για ανταλλαγή εκτυπώσεων, συν ppsPdfE, ppsPdfR και ppsPdfVT για εργασίες μηχανικής, ράστερ (raster) και μεταβλητών δεδομένων. Μέσα σε μια οικογένεια, ο κινητήρας (engine) διαβάζει το επίπεδο συμμόρφωσης που ισχυρίζεται το έγγραφο και το αναφέρει ανά πρότυπο στο ConformanceName του αποτελέσματος. Η ονομασία της οικογένειας είναι σπάνια αρκετή, επειδή το επίπεδο είναι εκεί που βρίσκεται η πραγματική διαφορά. Το PDF/A-2b υπόσχεται οπτική αναπαραγωγιμότητα και τίποτα περισσότερο. Το PDF/A-3a προσθέτει μια απαίτηση για ετικέτες λογικής δομής (logical structure tagging) και επιτρέπει ενσωματωμένα πηγαία αρχεία, το οποίο είναι ένα πολύ πιο δύσκολο εμπόδιο να ξεπεραστεί για σαρωμένο υλικό που δεν έχει καθόλου δέντρο ετικετών (tag tree). Κάντε λάθος προς οποιαδήποτε κατεύθυνση και η δέσμη (batch) σας λέει ψέματα. Αν η πολιτική διατήρησής σας θέλει πραγματικά PDF/A-2b, αλλά εσείς απορρίπτετε αρχεία για ελλείπουσες ετικέτες δομής, η αναφορά γεμίζει με ευρήματα που δεν θα διορθώσει ποτέ κανείς. Αποδεχτείτε οποιαδήποτε ετικέτα PDF/A χωρίς να ελέγξετε το επίπεδο και θα υπογράψετε για έγγραφα που πληρούν ασθενέστερα κριτήρια από αυτά που υποσχεθήκατε. Οι εντολές προσβασιμότητας από κυβερνητικούς αγοραστές στοιβάζουν όλο και περισσότερο το PDF/UA πάνω σε όλα αυτά, το οποίο δεν προσθέτει κανένα κόστος στην εκτέλεση, επειδή η BuildPdfPreflightReport (από τη μονάδα FPdfPreflightReport) δέχεται ένα σύνολο προτύπων:
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
Μία κλήση αξιολογεί και τα δύο πρότυπα και επιστρέφει μια μοναδική, ενοποιημένη εγγραφή αναφοράς
Γιατί μια κενή λίστα ευρημάτων δεν σημαίνει επιτυχία (pass)
Η αναφορά απαριθμεί τα ευρήματα ανά πρότυπο, και μια κενή λίστα προβλημάτων σημαίνει μόνο ότι "δεν βρέθηκαν προβλήματα στα πρότυπα που έτρεξαν πραγματικά". Αυτός είναι ένας στενότερος ισχυρισμός από το "το αρχείο συμμορφώνεται με το πρότυπο που σας ενδιαφέρει", και το χάσμα μεταξύ των δύο είναι εκεί όπου το batch preflight σαπίζει αθόρυβα. Ένα τυπογραφικό λάθος διαμόρφωσης (configuration) που παραλείπει το ppsPdfA από το σύνολο παράγει ακριβώς την ίδια κενή λίστα προβλημάτων με ένα πραγματικά καθαρό αρχείο. Άρα αντιμετωπίστε τη σιωπή ως ύποπτη. Διατρέξτε το Report.Results και βεβαιωθείτε για δύο πράγματα για κάθε πρότυπο που σκοπεύατε να ελέγξετε: ότι υπάρχει καθόλου μια καταχώρηση αποτελέσματος για αυτό, και ότι η σημαία (flag) IsCompliant, που υποστηρίζεται από το Status = pfsPass, είναι αληθής. Μια νυχτερινή εργασία που εξισώνει το "χωρίς ευρήματα" με το "έτοιμο για αρχειοθέτηση" χωρίς ποτέ να επιβεβαιώνει ποια πρότυπα αξιολογήθηκαν, είναι ο κλασικός τρόπος για να περάσει αθόρυβα ένας φάκελος με μη συμμορφούμενα αρχεία για μήνες, μέχρι ένας εξωτερικός ελεγκτής να ανοίξει ένα με το veraPDF και να τεθεί υπό αμφισβήτηση ολόκληρο το αρχείο
Μια δεύτερη παγίδα κρύβεται στο τι καν είναι ένα εύρημα. Κάθε TPdfPreflightIssue φέρει έναν Κωδικό (Code), μια Κατηγορία (Category), μια Περιγραφή (Description) και μια Σύσταση (Recommendation), και ονομάζει τον κανόνα που παραβιάστηκε, όχι μια σελίδα ή ένα αντικείμενο. Αυτή είναι μια επιλογή σχεδιασμού με συνέπειες για τον βρόχο ανατροφοδότησης. Η αναφορά λέει στην ομάδα παραγωγής ποια κατηγορία ελαττώματος υπάρχει, π.χ. μια μη ενσωματωμένη γραμματοσειρά ή ένα αναγνωριστικό XMP που λείπει, και η εύρεση του συγκεκριμένου παραβατικού αντικειμένου είναι δουλειά του εργαλείου αποκατάστασης κατάντη (downstream), όχι του επικυρωτή (validator). Κατασκευάστε τους καταναλωτές αναφορών σας βάσει των σταθερών τιμών Code, ποτέ βάσει του αναγνώσιμου από τον άνθρωπο κειμένου περιγραφής, το οποίο μπορεί να αναδιατυπωθεί μεταξύ των κυκλοφοριών χωρίς προειδοποίηση
Αρχεία αναφοράς για μηχανές και για το άτομο που έχει εφημερία (on call)
Η εγγραφή αναφοράς γράφει τα ίδια ευρήματα σε πέντε μορφές: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile και SaveMarkdownToFile, καθεμία με μια αντίστοιχη συνάρτηση τύπου ToJson όταν θέλετε τη συμβολοσειρά στη μνήμη αντί για το δίσκο. Αντισταθείτε στην παρόρμηση να επιλέξετε μία. Γράψτε JSON για τη διοχέτευση (pipeline), έτσι ώστε το CI να μπορεί να το επισυνάψει στην εγγραφή εργασίας και να αναλύσει (parse) τους κωδικούς προβλημάτων και τις καταστάσεις ανά πρότυπο χωρίς να αποξέει (scrape) κείμενο. Γράψτε HTML για τον άνθρωπο που ειδοποιείται, επειδή ανοίγει σε οποιοδήποτε πρόγραμμα περιήγησης χωρίς καθόλου εργαλεία. Τα δύο μαζί κοστίζουν μία επιπλέον γραμμή ανά αρχείο και γλιτώνουν τον μηχανικό εφημερίας (on-call engineer) από τη χειρότερη εργασία στη μαζική επεξεργασία (batch processing), η οποία είναι η αντίστροφη μηχανική (reverse-engineering) ενός ακατέργαστου JSON blob στις δύο το πρωί για να μάθει ποιο αρχείο έσπασε. Μια πειθαρχία έχει μεγαλύτερη σημασία από την επιλογή μορφής: παράγετε κάθε όνομα αναφοράς από το όνομα του αρχείου εισόδου, ποτέ από μια χρονική σήμανση (timestamp), διαφορετικά δύο παράλληλες εκτελέσεις θα παρεμβάλουν αναφορές που δεν θα μπορείτε πλέον να αντιστοιχίσετε πίσω στις εισόδους τους
Τα όρια (thresholds) σοβαρότητας ανήκουν στη διαμόρφωση (configuration) και όχι στον κώδικα. Ένας σχολιασμός χωρίς εναλλακτική περιγραφή αποτελεί σκληρή αποτυχία (hard failure) για μια πύλη υποβολής PDF/UA και μια αγνοήσιμη σημείωση για ένα εσωτερικό αρχείο, ωστόσο είναι το ίδιο ακριβώς εύρημα και στα δύο. Εκθέστε ένα επίπεδο fail-on ανά προφίλ, ώστε η πολιτική να μπορεί να αλλάξει χωρίς επαναμεταγλώττιση (recompile), και σφραγίστε το επίπεδο που ίσχυε στην ίδια τη σύνοψη (summary) της εργασίας. Το επόμενο τρίμηνο κανείς δεν θα θυμάται με ποιο όριο (threshold) έτρεξε η παρτίδα (batch) του περασμένου Οκτωβρίου, και η σύνοψη είναι το μόνο μέρος όπου επιβιώνει αυτή η μνήμη
Απομόνωση αρχείων ώστε ένα κακό PDF να μην μπορεί να βυθίσει τη δέσμη
procedure RunPreflightBatch(const InputDir, ReportDir: string;
out FilesWithFindings, ToolFailures: Integer);
var
SR: TSearchRec;
Pdf: TPdf;
Report: TPdfPreflightReport;
begin
FilesWithFindings := 0;
ToolFailures := 0;
if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
try
repeat
Pdf := TPdf.Create(nil); // fresh instance per file: no state bleed
try
try
Pdf.FileName := InputDir + SR.Name;
Pdf.Active := True;
if not Pdf.Active then // load failures are silent, not raised
raise EPdfError.Create('Cannot open ' + SR.Name);
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
if Report.TotalIssueCount > 0 then
Inc(FilesWithFindings);
except
on E: Exception do
begin
Inc(ToolFailures); // exit-code-2 territory, not a validation verdict
WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
end;
Τρεις σκόπιμες επιλογές ζουν σε αυτόν τον βρόχο. Ένα φρέσκο TPdf ανά αρχείο εγγυάται ότι ένα έγγραφο που διαφθείρει την κατάσταση του κινητήρα δεν μπορεί να δηλητηριάσει τα αρχεία που το ακολουθούν. Ο ρητός έλεγχος (explicit check) Active κερδίζει τη θέση του επειδή το Active := True καταπίνει τα σφάλματα φόρτωσης αντί να τα εγείρει· αν ρίξετε την προστασία (guard), ένα περικομμένο (truncated) αρχείο θα παρασυρθεί στην κλήση επικύρωσης πριν αποτύχει κάπου κατάντη (downstream) με ένα παραπλανητικό μήνυμα. Το εσωτερικό try..except ζει εσκεμμένα μέσα στο πεδίο ισχύος (scope) ανά αρχείο, έτσι ώστε μια μόνο εξαίρεση να αυξάνει τον μετρητή αποτυχιών και ο βρόχος να συνεχίζεται. Θέλετε καθαρές αναφορές για τα 4.999 καλά αρχεία ακόμη και όταν το αρχείο 5.000 έχει καταστραφεί. Και οι δύο μορφές αναφοράς γράφονται στο δίσκο πριν από την καταμέτρηση της ετυμηγορίας (verdict), πράγμα που σημαίνει ότι τα αποδεικτικά στοιχεία επιβιώνουν ακόμη και αν ένα σφάλμα (bug) αργότερα στη λογική της σύνοψης κάνει λάθος μέτρηση
Η αντιστοίχιση του κωδικού εξόδου (exit-code) στη συνέχεια καταρρέει (collapses) σε μερικές γραμμές στο αρχείο του έργου (project file):
begin
RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
if Failures > 0 then
Halt(2)
else if Findings > 0 then
Halt(1);
// falling through exits with 0: every file conformed
end.
Τι δεν θα κάνει το preflight για εσάς
Ο κινητήρας (engine) ανιχνεύει· δεν επισκευάζει. Ένα εύρημα σχετικά με μια μη ενσωματωμένη γραμματοσειρά ή έναν χρωματικό χώρο (color space) εξαρτώμενο από τη συσκευή είναι μια εντολή εργασίας για όποιον παράγει τα αρχεία, και ο επικυρωτής (validator) δεν έχει τρόπο να το διορθώσει επιτόπου. Σχεδιάστε λοιπόν τον βρόχο ανατροφοδότησης (feedback loop) σκόπιμα. Οι αναφορές πρέπει να προσγειώνονται εκεί όπου τις διαβάζει πραγματικά η ομάδα παραγωγής, αλλιώς τα ίδια ευρήματα θα επανεμφανίζονται κάθε βράδυ μέχρι να ρωτήσει τελικά κάποιος γιατί το ποσοστό συμμόρφωσης δεν βελτιώνεται ποτέ. Αξίζει επίσης να διασταυρώσετε (cross-check) ένα δείγμα ετυμηγοριών με έναν ανεξάρτητο επικυρωτή, το veraPDF για το PDF/A ή το preflight του Acrobat για το PDF/X, προτού ένας εξωτερικός ελεγκτής τα διασταυρώσει για εσάς. Όταν δύο κινητήρες διαφωνούν για ένα πραγματικό αρχείο πελάτη, αυτό το έγγραφο δεν αποτελεί ενόχληση· είναι ακριβώς η περίπτωση υποβάθμισης (regression) που έλειπε από τις δοκιμές κυκλοφορίας σας. Κρατήστε το, ονομάστε το και τρέξτε το σε κάθε κατασκευή (build)
Αξίζει να γνωρίζετε έναν ακόμη συνδυασμό. Ο ίδιος κινητήρας επικύρωσης οδηγεί τους διαδραστικούς ελέγχους σε ένα UI αναθεώρησης, οπότε αυτό το ακέφαλο (headless) CLI και ένας πάγκος εργασίας ελέγχου εισαγωγής PDF (PDF intake review workbench) που απευθύνεται σε αναλυτές, μπορούν να μοιράζονται ένα κοινό λεξιλόγιο επικύρωσης αντί να απομακρύνονται με την πάροδο του χρόνου. Και επειδή το [ppsPdfA, ppsPdfUa] αξιολογεί την προσβασιμότητα στο ίδιο πέρασμα, η πλευρά PDF/UA της δέσμης ευθυγραμμίζεται καθαρά με την εργασία στην πλευρά του προγράμματος προβολής, όπως η δημιουργία ενός προσβάσιμου προγράμματος ανάγνωσης PDF στο Delphi. Τα προφίλ, οι μορφές αναφοράς και το πλήρες API του preflight τεκμηριώνονται στη σελίδα προϊόντος για το PDFium Component