Ένας έλεγχος εισαγωγής αρχείων απέρριψε μια παρτίδα αρχείων "PDF/A-2b" που άνοιγαν μια χαρά σε κάθε προβολέα του γραφείου. Ο προμηθευτής ορκιζόταν πως ήταν συμβατά. Δεν ήταν: το καθένα έκρυβε μια ενέργεια JavaScript βαθιά μέσα στον κατάλογο, το είδος που ένα πρόχειρο βλέμμα δεν πιάνει ποτέ και ένας πλήρης validator PDF/A, όπως το veraPDF, επισημαίνει ακαριαία. Το πρόβλημα είναι ότι κανείς δεν ήθελε να κουμπώσει μια αλυσίδα εργαλείων Java πάνω σε μια υπηρεσία μαζικής επεξεργασίας Delphi μόνο και μόνο για να απαντήσει σε μια ερώτηση ναι ή όχι για κάθε αρχείο. Αυτό είναι το κενό ValidatePdfACompliance που καλύπτει το PDFium Component, και αξίζει να καταλάβει κανείς πώς φτάνει σε μια κρίση χωρίς ποτέ να αναλύει πλήρως μια ροή περιεχομένου
Γιατί το ίδιο το PDFium δεν μπορεί να απαντήσει σε αυτό
Το πρώτο πράγμα που πρέπει να ειπωθεί με ειλικρίνεια είναι ότι το ενσωματωμένο pdfium.dll δεν έχει καθόλου δυνατότητα PDF/A. Δεν υπάρχει ConvertToPDFA, ούτε writer OutputIntent, ούτε API XMP στη δημόσια επιφάνεια. Κάθε μέρος του PDF/A σε αυτή τη βιβλιοθήκη, τόσο η πλευρά της εγγραφής όσο και η πλευρά του ελέγχου, υλοποιείται σε καθαρό Pascal μέσα στο FPdfPdfa.pas και δουλεύει με ανάλυση σε επίπεδο byte μαζί με σταδιακές ενημερώσεις. Άρα, όταν καλείτε τον validator δεν ρωτάτε τίποτα τον renderer του Chromium. Τρέχετε έναν Pascal token scanner πάνω στα δομικά bytes του αρχείου
Το δημόσιο API είναι σκόπιμα μικρό. Μια συνάρτηση διαβάζει μια ροή από τη θέση 0 και επιστρέφει μια εγγραφή:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant κωδικοποιεί τον κανόνα που έχει σημασία σε έναν έλεγχο: ένα αρχείο περνά μόνο όταν ανιχνεύτηκε πραγματικό επίπεδο συμμόρφωσης και το σύνολο των ζητημάτων είναι κενό. Μια ανάλυση που ολοκληρώνεται επιτυχώς αλλά δεν βρίσκει δείκτη pdfaid καταλήγει σε pacNone, το οποίο ρητά δεν είναι επιτυχία. Αυτό είναι το ίδιο συμπέρασμα που βγάζει το batch preflight report CLI από έξω: μια κενή λίστα ευρημάτων σε ένα μη αναγνωρισμένο αρχείο δεν είναι καθαρή βεβαίωση υγείας
Απογύμνωση των σωμάτων των ροών πριν από κάθε σάρωση tokens
Να η πιο σημαντική λεπτομέρεια υλοποίησης, και αυτή που είναι πιο εύκολο να γίνει λάθος αν γράψετε δικό σας scanner. Ο ανιχνευτής βρίσκει παραβιάσεις αναζητώντας οριοθετημένα name tokens, πράγματα όπως /JavaScript, /LZWDecode, /BM. Αν σαρώσετε τα ακατέργαστα bytes του αρχείου, τα ενσωματωμένα δυαδικά σώματα ροών, οι συμπιεσμένες εικόνες, τα προφίλ ICC, τα προγράμματα γραμματοσειρών, θα περιέχουν τυχαία ακολουθίες bytes που μοιάζουν με αυτά τα tokens. Θα αναφέρετε /AA ή /3D "found" επειδή τρία bytes μέσα σε ένα JPEG έτυχε να το σχηματίζουν. Αυτό είναι εργοστάσιο ψευδώς θετικών
Η λύση είναι PdfStructureBytes: περνάει μέσα από το αρχείο και μηδενίζει τα bytes ανάμεσα σε κάθε stream και endstream λέξη-κλειδί, αφήνοντας άθικτη τη δομή του λεξικού. Μόνο τότε εκτελείται η σάρωση. Κάθε έλεγχος name-token στον validator λειτουργεί πάνω σε αυτό το απογυμνωμένο αντίγραφο. Αν κρατήσετε μία ιδέα από αυτό το άρθρο, κρατήστε αυτήν. Η ίδια πειθαρχία αντικατοπτρίζεται στον validator PDF/UA, ο οποίος διατηρεί το δικό του αντίγραφο της ρουτίνας επειδή τα δύο πρότυπα εξελίσσονται ανεξάρτητα
Τα 29 ζητήματα και τι σημαίνει το καθένα
TPdfAValidationIssue είναι τεκμηριωμένο συμβόλαιο. Οι αύξοντες αριθμοί είναι παγιωμένοι επειδή τα τεστ DUnitX, τα demos και το επίπεδο αναφορών εξαρτώνται όλα από αυτούς, οπότε τα νέα ευρήματα προστίθενται μόνο στο τέλος. Από την έκδοση v1.63.0 υπάρχουν 29 μέλη. Χωρίζονται σε λίγες οικογένειες:
- Μεταδεδομένα και ταυτότητα:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Χρώμα και έξοδος:
pvaiMissingOutputIntent,pvaiMissingIccProfile, καιpvaiMixedDeviceColorSpacesόταν εμφανίζονται και τα DeviceRGB και τα DeviceCMYK (6.2.3.3) - Αυστηρές απαγορεύσεις για κάθε μέρος:
pvaiEncryptionPresent(ένα/Encryptλεξικό απαγορεύεται πλήρως),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Γραμματοσειρές:
pvaiFontNotEmbeddedκαι το αυστηρότεροpvaiUnembeddedFont, καθώς καιpvaiUnicodeMappingMissingγια μια δήλωση Level U χωρίς/ToUnicode - Σήμανση:
pvaiLevelAStructureMissingόταν μια δήλωση συμμόρφωσης=A δεν έχει δομή με ετικέτες
Τα έξι νεότερα μέλη, που προστέθηκαν στις θέσεις 24 έως 29, καλύπτουν τις λεπτές περιπτώσεις όπου οι αξιολογητές σκοντάφτουν πραγματικά: pvaiTrappedTrue (ένα /Trapped /True στο λεξικό Info, ένα «false friend» αφού η τιμή πρέπει να είναι False ή Unknown), pvaiForbiddenActionSubtype (το Sound ή το Movie χρησιμοποιούνται ως ενέργεια, όχι μόνο ως annotation), pvaiTransparentColorSpace (μια λειτουργία ανάμειξης εκτός Normal ή ένα /CA//ca που δεν ισούται με 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, και pvaiMixedDeviceColorSpaces
Έλεγχος με επίγνωση του μέρους: το A-1 είναι αυστηρό, το A-2 και το A-3 χαλαρώνουν
Το PDF/A δεν είναι ένα μόνο βιβλίο κανόνων. Τρία πράγματα που το PDF/A-1 απαγορεύει επιτρέπονται ρητά από το PDF/A-2 και μετά: διαφάνεια (ένα /Transparency group ή ένα ενεργό /SMask, 6.4), προαιρετικό περιεχόμενο (/OCProperties, 6.1.13), και ενσωματωμένα αρχεία (/EmbeddedFiles ή /EF). Ένας αφελής επικυρωτής που σημαδεύει και τα τρία για κάθε αρχείο θα απορρίψει μαζικά απολύτως έγκυρα έγγραφα PDF/A-2
Έτσι ο επικυρωτής διαβάζει τον αριθμό του μέρους από τον δείκτη pdfaid μέσω PdfAPartOf και ενεργοποιεί αυτούς τους ελέγχους μόνο όταν PartNo = 1. Οι έλεγχοι blend-mode και annotation-alpha για τα νέα ζητήματα διαφάνειας είναι επίσης μόνο για το part 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Αξίζει να αναφερθεί μια συντηρητική προεπιλογή: όταν δεν υπάρχει καθόλου δείκτης pdfaid, το part θεωρείται 1, το πιο αυστηρό. Η λογική είναι ότι ένα μη αναγνωρισμένο αρχείο πρέπει να υπόκειται στους πιο αυστηρούς κανόνες και όχι να περνάει απλώς. Το JavaScript, οι forbidden actions, το LZW, το XFA, το NeedAppearances, οι forbidden annotations και οι unembedded fonts παραμένουν απαγορευμένα για κάθε part, οπότε αυτοί οι έλεγχοι δεν μπαίνουν ποτέ πίσω από την πύλη
Επέκταση των object streams ώστε να μην κρύβεται τίποτα
Το PDF 1.5 εισήγαγε το cross-reference stream και το object stream (/Type /ObjStm), και δημιουργούν ένα τυφλό σημείο για έναν αφελή byte scanner. Ένα catalog, ένα OutputIntent, ένα action dictionary, οτιδήποτε δεν είναι stream από μόνο του, μπορεί να είναι Flate-compressed μέσα σε ένα ObjStm. Σάρωσε την ακατέργαστη δομή και δεν θα δεις τίποτα από αυτά, για να αναφέρεις ένα καθαρό αρχείο που δεν είναι καθόλου καθαρό
PdfExpandObjectStreams κλείνει αυτό το κενό. Πριν εκτελεστεί οποιοσδήποτε έλεγχος, ο validator κάνει Data := PdfExpandObjectStreams(Data). Η ρουτίνα βρίσκει κάθε ObjStm, διαβάζει τα /N και /First κεφαλίδα για να πάρει τους αριθμούς και τις μετατοπίσεις των περιεχόμενων αντικειμένων, φουσκώνει το σώμα με PdfInflate (το RTL zlib, System.ZLib στο Delphi και zstream στο FPC), και προσθέτει κάθε περιεχόμενο αντικείμενο ως κανονικό N 0 obj ... endobj στο τέλος ενός αντιγράφου των bytes. Οι υπάρχοντες έλεγχοι token βρίσκουν τότε αυτά τα αντικείμενα χωρίς καμία αλλαγή στη λογική τους
Δύο περιορισμοί κάνουν αυτό καθαρό και όχι εύθραυστο. Τα stream objects, το Metadata, το ICC profile και τα font programs δεν μπορούν να βρίσκονται σε object stream, μόνο τα non-stream dictionaries μπορούν, έτσι η επέκταση ασχολείται μόνο με dictionaries και τα προσαρτημένα αντικείμενα δεν φέρουν κανένα stream keyword που θα διατάρασσε το βήμα αφαίρεσης του σώματος. Και επειδή το προσαρτημένο περιεχόμενο τοποθετείται μετά το %%EOF, η αντίστροφη αναζήτηση από το startxref εξακολουθεί να βρίσκει το αρχικό trailer. Το ίδιο το trailer του cross-reference stream είχε ήδη αντιμετωπιστεί νωρίτερα, στο v1.49.3, με την απευθείας ανάγνωση των Root, Size και ID από το plaintext xref-stream dictionary, θέμα που εξερευνάται στο συνοδευτικό άρθρο για το την επικύρωση object και cross-reference streams; η δουλειά για τα object-streams χρειάστηκε μόνο να προσθέσει το βήμα inflate, χωρίς να αποκωδικοποιήσει type-2 xref entries ή να αναιρέσει έναν PNG predictor
Τα ειλικρινή όρια ενός ελεγκτή σε επίπεδο bytes
Αυτό είναι ένα εργαλείο preflight, όχι ένας πιστοποιημένος validator, και τα όρια είναι πραγματικά. Η ενσωμάτωση γραμματοσειρών είναι μια ευρετική μέτρησης, και το να γίνει σωστά χρειάστηκε μια διόρθωση που αξίζει να γνωρίζετε. Ο αρχικός έλεγχος χρησιμοποιούσε PdfCountName('/FontDescriptor'), αλλά κάθε γραμματοσειρά συνεισφέρει δύο /FontDescriptor tokens, μία αναφορά από το font dictionary και μία /Type στο ίδιο το descriptor object, έτσι η μέτρηση ήταν 2N έναντι N embedded programs και το τεστ ήταν πάντα αληθές. Η διόρθωση είναι PdfCountDescriptorRefs, το οποίο μετρά μόνο τη μορφή αναφοράς /FontDescriptor N G R reference, μία ανά γραμματοσειρά, και εγείρει pvaiUnembeddedFont μόνο όταν τα ενσωματωμένα προγράμματα είναι πραγματικά λιγότερα:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Ακόμη και διορθωμένο, παραμένει χονδροειδές: ένα μικτό έγγραφο όπου κάθε descriptor τυχαίνει να έχει κάποιο FontFile μπορεί ακόμη να αφήσει μια μεμονωμένη μη συμμορφούμενη γραμματοσειρά να περάσει. Η επέκταση των object streams έχει επίσης μια γνωστή παρενέργεια, εκθέτει τα standard-14 default resources που φέρει ένα AcroForm /DR /DR, όπως /Helv, και η ευρετική τα αναφέρει ευσυνείδητα ως μη ενσωματωμένα, παρότι το veraPDF τα περνά επειδή δεν χρησιμοποιούνται ποτέ πραγματικά για απόδοση. Οι έλεγχοι σε επίπεδο operator των content-stream (6.2.10) είναι εντελώς εκτός πεδίου, αφού θα χρειάζονταν πλήρη ανάλυση περιεχομένου και όχι byte scan. Αντιμετωπίστε τον validator ως μια γρήγορη, χωρίς εξαρτήσεις πρώτη πύλη που εντοπίζει τις παραβιάσεις που η έγχυση marker δεν μπορεί να διορθώσει, και κρατήστε έναν πλήρη validator για την τελική πιστοποίηση
Αυτό είναι το τμήμα ελέγχου της ιστορίας. Η συμπληρωματική πλευρά εγγραφής, όπου SaveAsPdfA εισάγει το XMP, το OutputIntent και το sRGB ICC profile και υποβαθμίζει με ειλικρίνεια ένα αίτημα Level A που δεν έχει tagged structure, βασίζεται στον ίδιο μηχανισμό σε επίπεδο bytes. Και τα δύο μέρη διατίθενται στο PDFium Component for Delphi, ένα ενιαίο VCL package πάνω σε μια pure-Pascal υλοποίηση PDF/A χωρίς εξωτερικό runtime για εγκατάσταση