Ένα workbench ελέγχου εισαγωγής PDF είναι ένα μικρό πρόγραμμα με μία δουλειά: να κοιτάξει κάθε αρχείο πριν επιτραπεί σε οτιδήποτε παρακάτω στη ροή να το αγγίξει. Για να κάνει αυτή τη δουλειά πρέπει να συναρμολογήσει μια χούφτα δυνατότητες σε ένα πέρασμα. Ανοίγει το αρχείο (χωρίς να το εμπιστεύεται), διαβάζει τι ισχυρίζεται το αρχείο για τον εαυτό του, ψάχνει για περιεχόμενο που θα παραπλανήσει έναν αφελή εξαγωγέα ή θα μεταφέρει μια επίθεση, αποφασίζει αν υπάρχει καθόλου εξαγώγιμο κείμενο, και μετά δρομολογεί το έγγραφο σε μια ουρά με βάση αυτό που βρήκε. Παραλείψτε την επιθεώρηση και οι αστοχίες είναι σιωπηλές: ένα PDF κρυπτογραφημένο με κωδικό ιδιοκτήτη που τυλίγει μια φόρμα XFA περνά μέσα από έναν εξαγωγέα κειμένου ως κενά strings, καταχωρείται ως κενό έγγραφο, και κανείς δεν το προσέχει μέχρι κάποιος παρακάτω στη ροή να ψάξει για περιεχόμενο που ποτέ δεν διαβάστηκε. Το PDFium Component είναι μια βιβλιοθήκη viewer και επιθεώρησης VCL/LCL με πηγαίο κώδικα για Delphi, C++Builder και Lazarus, και εκθέτει τις κλήσεις ενδοσκόπησης που χρειάζεται αυτό το workbench. Οι παρακάτω ενότητες περνούν από το ποια κλήση απαντά σε ποια ερώτηση, και τα δύο σημεία όπου η προφανής κλήση σας δίνει μια σίγουρα λανθασμένη απάντηση
Πέντε ερωτήσεις για απάντηση πριν δρομολογηθεί ένα αρχείο
Αφαιρέστε το πλέγμα και τη λωρίδα μικρογραφιών, και η διαλογή εισαγωγής ανάγεται σε πέντε ερωτήσεις:
- Μπορεί το αρχείο να ανοίξει καθόλου, και κάτω από ποιον κωδικό πρόσβασης?
- Τι ισχυρίζεται ότι είναι: τίτλος, συγγραφέας, ημερομηνία δημιουργίας?
- Φέρει ενεργό ή επικίνδυνο περιεχόμενο όπως JavaScript, φόρμα XFA, ή ενσωματωμένα αρχεία?
- Υπάρχει εξαγώγιμο κείμενο, ή πρόκειται για σάρωση προς OCR?
- Δεδομένων όλων αυτών, ποια ουρά το παίρνει: απευθείας επεξεργασία, χειροκίνητη αναθεώρηση, ή καραντίνα?
Κάθε ερώτηση αντιστοιχεί σε μία ή δύο κλήσεις PDFium Component. Δύο από αυτές τις αντιστοιχίσεις έχουν αιχμηρές γωνίες που ευθύνονται για τα περισσότερα λανθασμένα δρομολογημένα αρχεία που έχω χρειαστεί να αποσφαλματώσω σε παραγωγή. Τα μεταδεδομένα εγγράφου ζουν σε δύο διαφορετικά σημεία που μπορεί να διαφωνούν, και η κρυπτογράφηση δεν εμποδίζει απαραίτητα ένα έγγραφο να ανοίξει
Ανοίξτε φθηνά: form fill απενεργοποιημένο, μηδέν σελίδες αποδοσμένες
Η διαλογή θα πρέπει να είναι το φθηνότερο δυνατό άνοιγμα. Ο ορισμός FormFill := False πριν το Active := True λέει στο component να παραλείψει εντελώς το περιβάλλον form-fill. Αυτό συντομεύει τον χρόνο φόρτωσης, και (εξίσου σημαντικό για αρχεία άγνωστης προέλευσης) εμποδίζει την αρχικοποίηση οποιασδήποτε JavaScript σε επίπεδο εγγράφου. Καμία από τις παρακάτω ιδιότητες επιθεώρησης δεν απαιτεί απόδοση σελίδας, οπότε ένα πέρασμα διαλογής δεν χρειάζεται ποτέ να παράγει ούτε ένα bitmap
procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := IncomingPath;
Pdf.FormFill := False; // κανένα περιβάλλον φόρμας, καμία αρχικοποίηση JavaScript
Pdf.Active := True; // η αποτυχία είναι σιωπηλή: το Active απλώς παραμένει False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // κατεστραμμένο αρχείο ή κλείδωμα με κωδικό χρήστη
Exit; // το μπλοκ finally εκτελείται ούτως ή άλλως
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // ποτέ μην αφήνετε το instance να διαρρεύσει σε κατεστραμμένο αρχείο
end;
end;
Ο έλεγχος μετά την ανάθεση δεν είναι προαιρετικός, και είναι έλεγχος παρά χειριστής εξαίρεσης για έναν λόγο. Όταν η μηχανή δεν μπορεί να φορτώσει το αρχείο, το component καταπίνει την εσωτερική EPdfError και αφήνει το Active σε False αντί να τη διαδώσει. Κώδικας που περιμένει μια εξαίρεση θα διαβάσει ευχαρίστως το PageCount από ένα έγγραφο που ποτέ δεν άνοιξε. Αν η ροή απόρριψης χρειάζεται το πραγματικό κείμενο σφάλματος της μηχανής, διαβάστε το αρχείο σε έναν πίνακα byte και καλέστε την υπερφόρτωση LoadDocument που δέχεται TBytes· αυτή η διαδρομή πράγματι εγείρει EPdfError με το μήνυμα, συμπεριλαμβανομένης της περίπτωσης κωδικού. Το try..finally εξακολουθεί να κερδίζει τη θέση του. Οι υπηρεσίες εισαγωγής τρέχουν χωρίς επίβλεψη για εβδομάδες, και καμία μεταγενέστερη εξαίρεση δεν πρέπει να αφήνει το instance TPdf να διαρρεύσει ή να κρατά ένα κλείδωμα που το πέρασμα επανάληψης θα σκοντάψει
Η ταχύτητα σπάνια γίνεται το σημείο συμφόρησης. Με το form fill απενεργοποιημένο και χωρίς απόδοση, ένα άνοιγμα διαλογής κυριαρχείται από I/O, και ένας μόνο worker επιθεωρεί άνετα αρκετά αρχεία ανά δευτερόλεπτο από τοπικό δίσκο. Αν ο όγκος εισαγωγής κάποτε ξεπεράσει έναν worker, διαμερίστε τη δουλειά ανά αρχείο και όχι ανά έλεγχο. Οι πέντε ερωτήσεις μοιράζονται ένα άνοιγμα, και ο διαμερισμός τους σε διεργασίες θα πολλαπλασίαζε το πιο ακριβό βήμα αντί να το αποσβήνει
Τα μεταδεδομένα ζουν σε δύο σημεία, και διαφωνούν
Το ISO 32000-1 ορίζει δύο σπίτια για τα μεταδεδομένα εγγράφου: το document information dictionary (ρήτρα 14.3.3) και ένα πακέτο XMP προσαρτημένο στον κατάλογο (ρήτρα 14.3.2). Οι ιδιότητες Title, Author, Subject και CreationDate διαβάζουν το Info dictionary, με το MetaText[] για οποιοδήποτε άλλο κλειδί και το DecodeDate για ανάλυση του string ημερομηνίας D:YYYYMMDD.... Η παγίδα είναι ότι οι σύγχρονοι παραγωγοί γράφουν όλο και περισσότερο μόνο XMP, μια κατεύθυνση που το ISO 32000-2 κάνει επίσημη καταργώντας τα περισσότερα κλειδιά του Info dictionary στο PDF 2.0. Το σύμπτωμα σε ένα εργαλείο εισαγωγής είναι συγκεκριμένο. Το workbench σας δείχνει κενό τίτλο ενώ το Adobe Acrobat εμφανίζει έναν, επειδή το Acrobat κατέφυγε στο dc:title μέσα στο πακέτο XMP, το οποίο οι ιδιότητες του Info dictionary ποτέ δεν αγγίζουν
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // τιμή Info dictionary
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // ακατέργαστο string ημερομηνίας PDF ("D:2026...")
// Ένας κενός τίτλος Info δεν σημαίνει ότι το έγγραφο είναι χωρίς τίτλο. Το
// component δεν εκθέτει το πακέτο XMP, οπότε ελέγξτε τα ακατέργαστα bytes
// του αρχείου για το στοιχείο dc:title πριν εμπιστευτείτε το κενό.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Ακόμη και ο πρόχειρος έλεγχος υποαλφαριθμητικού παραπάνω κερδίζει τη θέση του: "μεταδεδομένα υπάρχουν, αλλά όχι εκεί που κοιτούν τα παλιά εργαλεία" είναι ένα γεγονός σχετικό με τη δρομολόγηση για κάθε αρχειακή ροή εργασίας που κάνει ευρετηρίαση σε τίτλο ή συγγραφέα. Αν το downstream ευρετήριό σας διαβάζει μόνο το Info dictionary, αρχεία σημαδεμένα με αυτόν τον τρόπο θα γίνουν σιωπηλά μη αναζητήσιμα
Κρυπτογραφημένα αρχεία που ανοίγουν ούτως ή άλλως
Ένα κρυπτογραφημένο έγγραφο δεν αποτυγχάνει απαραίτητα να ανοίξει. Ο τυπικός χειριστής ασφαλείας (ISO 32000-1 ρήτρα 7.6.3) διακρίνει έναν κωδικό χρήστη, απαιτούμενο για το άνοιγμα του εγγράφου, από έναν κωδικό ιδιοκτήτη που απλώς φυλάει δικαιώματα όπως εκτύπωση και αντιγραφή. Ένα μεγάλο μερίδιο "προστατευμένων" επιχειρηματικών εγγράφων είναι κρυπτογραφημένα με κωδικό ιδιοκτήτη και κενό κωδικό χρήστη. Ανοίγουν χωρίς προτροπή, αποκρυπτογραφούνται πλήρως, και βασίζονται στους viewer να τηρήσουν εθελοντικά τις σημαίες δικαιωμάτων. Αυτό είναι πολιτική, όχι προστασία, και οι καταστάσεις εισαγωγής σας πρέπει να αντικατοπτρίζουν τη διαφορά
Ο εντοπισμός κρυπτογράφησης μετά από επιτυχές άνοιγμα παίρνει μία κλήση μηχανής συν ένα εφεδρικό μηχανισμό. Το FPDF_GetSecurityHandlerRevision(Pdf.Document) επιστρέφει -1 για μη προστατευμένα αρχεία και την αναθεώρηση χειριστή διαφορετικά, και το Pdf.Permissions να επιστρέφει οτιδήποτε άλλο εκτός από τη μάσκα με όλα τα bit ενεργά $FFFFFFFF είναι το επιβεβαιωτικό σήμα. Για αρχεία πραγματικά κλειδωμένα με κωδικό χρήστη, αναθέστε το Password πριν ορίσετε το Active := True· αν το άνοιγμα εξακολουθεί να αποτυγχάνει, δρομολογήστε το αρχείο σε κατάσταση αποκλεισμού που ζητά διαπιστευτήρια από τον αποστολέα μέσω ασφαλούς καναλιού αντί να επαναλαμβάνετε τυφλά. Και αντισταθείτε στον πειρασμό να αντιμετωπίζετε το "κρυπτογραφημένο" ως αυτόματη καραντίνα. Στις περισσότερες βιομηχανίες με πολλά έγγραφα, τα κρυπτογραφημένα-αλλά-ανοίξιμα αρχεία είναι η κανονική περίπτωση, όχι η ύποπτη
Ενεργό περιεχόμενο: JavaScript, XFA, και ενσωματωμένα αρχεία
Τρία ευρήματα θα πρέπει πάντα να φτάνουν στην απόφαση δρομολόγησης. Πρώτον, JavaScript: το event OnUnsupportedFeature αναφέρει δομικά χαρακτηριστικά όπως XFA ή περιεχόμενο 3D καθώς η μηχανή τα συναντά, αλλά δεν εντοπίζει JavaScript. Ελέγξτε αντ' αυτού το JavaScriptActionCount και αντιμετωπίστε ένα μη μηδενικό αποτέλεσμα ως ενεργό περιεχόμενο. Δεύτερον, XFA: όταν το FormType επιστρέφει ftXfaFull, οι ορατές σελίδες είναι συχνά λίγο περισσότερο από μια απόδοση του προτύπου XFA, και η συμβατική εξαγωγή κειμένου θα δει πρότυπο κείμενο αντί για τις συμπληρωμένες τιμές. Τρίτον, συνημμένα: ένα PDF είναι μορφή δοχείου, και το AttachmentCount σας λέει αν αυτό μεταφέρει επιβάτες
procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
i, PageNo: Integer;
Ext: string;
begin
Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
(FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
Rec.HasForms := Pdf.FormType <> ftNone;
Rec.IsXfa := Pdf.FormType = ftXfaFull;
Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;
// Το AnnotationCount είναι ιδιότητα ανά σελίδα· διατρέξτε τις σελίδες για να το
// αθροίσετε. Η φόρτωση αντικειμένου σελίδας δεν αποδίδει τίποτα, οπότε παραμένει φθηνό.
Rec.Annotations := 0;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
Inc(Rec.Annotations, Pdf.AnnotationCount);
end;
Rec.Attachments := Pdf.AttachmentCount;
for i := 0 to Rec.Attachments - 1 do
begin
Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
Include(Rec.Flags, ifDangerousAttachment);
end;
end;
Δύο λεπτομέρειες σε εκείνο το loop αξίζουν προσοχή. Το όνομα συνημμένου προέρχεται από μέσα από το έγγραφο, οπότε ποτέ μην το επαναχρησιμοποιείτε ως διαδρομή εξόδου χωρίς πρώτα να το εξυγιάνετε· ένα ενσωματωμένο όνομα όπως ..\..\start.exe είναι μια διάσχιση διαδρομής που περιμένει μια απρόσεκτη κλήση αποθήκευσης. Και μια λίστα αποκλεισμού επεκτάσεων είναι μια παγίδα συναγερμού, όχι μια εγγύηση. Η δουλειά της είναι να επιβάλει μια ανθρώπινη απόφαση, όχι να πιστοποιήσει το αρχείο καθαρό
Μετατροπή σημάτων σε καταστάσεις δρομολόγησης
Ένα λειτουργικό μοντέλο κατάστασης χρειάζεται λιγότερες καταστάσεις από όσες περιμένουν οι περισσότερες ομάδες: έτοιμο (κανένα εμπόδιο, κείμενο παρόν), αναθεώρηση (το άνοιγμα πέτυχε αλλά κάτι χρειάζεται μάτια, όπως μια φόρμα XFA, JavaScript, ένα κενό επίπεδο κειμένου, ή τίτλος μόνο σε XMP), αποκλεισμένο (απαιτείται κωδικός χρήστη), και κατεστραμμένο (το άνοιγμα απέτυχε). Καταγράψτε τα στοιχεία μαζί με την κατάσταση. Το hash αρχείου, ο αριθμός σελίδων, οι ακριβείς σημαίες, και το μήνυμα σφάλματος της μηχανής για κατεστραμμένα αρχεία έχουν όλα σημασία, επειδή το άτομο που θα αμφισβητήσει μια απόφαση δρομολόγησης θα το κάνει εβδομάδες αργότερα, ενάντια σε ένα αρχείο που ενδέχεται στο μεταξύ να έχει αντικατασταθεί ή τροποποιηθεί
Όταν ένας χειριστής χρειάζεται πράγματι να δει ένα αρχείο σε καραντίνα, μην του το δίνετε στον προεπιλεγμένο viewer του κελύφους. Αποδώστε το μέσα σε ένα θωρακισμένο πάνελ με scripting και χειρισμό συνδέσμων απενεργοποιημένα, την προσέγγιση που περιγράφεται στο χτίζοντας μια ασφαλή επιφάνεια προεπισκόπησης PDF σε Delphi. Και αν η εισαγωγή σας τροφοδοτεί ένα αρχείο με απαιτήσεις συμμόρφωσης, το πέρασμα διαλογής είναι το φυσικό σημείο για να προγραμματίσετε έναν βαθύτερο έλεγχο· η μαζική επικύρωση preflight έναντι προφίλ PDF/A και PDF/UA ξεκινά ακριβώς εκεί όπου σταματά αυτή η επιθεώρηση
Η σελίδα προϊόντος του component καλύπτει την αδειοδότηση, το πλήρες API επιθεώρησης, και τα συνοδευτικά demos, συμπεριλαμβανομένου ενός επιθεωρητή εγγράφων τύπου εισαγωγής: PDFium Component