Τεχνικό Άρθρο

Συμμόρφωση αρχειοθέτησης PDF/A σε Delphi με το PDFium VCL

Παραδίδετε έναν μετατροπέα που χαρακτηρίζει κάθε αρχείο ως PDF/A-1b, το σύστημα εγγραφών του πελάτη τα εισάγει για έναν χρόνο, και ύστερα ένας έλεγχος περνά ολόκληρη τη δέσμη από το veraPDF και το ένα τρίτο επιστρέφει ως μη συμμορφούμενο. Τίποτα δεν κατέρρευσε, δεν σηκώθηκε εξαίρεση, τα αρχεία ανοίγουν κανονικά σε κάθε προβολέα πάνω στο γραφείο σας. Απλώς δεν ήταν το πρότυπο που τους αποδώσατε. Αυτή είναι η συνηθισμένη μορφή αποτυχίας για αρχειακό PDF, και γι' αυτό το «βάλαμε το flag» δεν ισοδυναμεί ποτέ με το «επαληθεύεται»

Το πρώτο που πρέπει να καταλάβετε για το PDFium και το PDF/A είναι ότι η μηχανή δεν έχει καμία σχέση με αυτό. Το PDFium αποδίδει, αναλύει και γράφει PDF, αλλά η δημόσια επιφάνειά του δεν έχει ConvertToPDFA, δεν έχει εγγραφέα OutputIntent, ούτε API XMP. Κάθε κομμάτι της συμμόρφωσης για αρχειακή χρήση, το πακέτο XMP, το OutputIntent και το ενσωματωμένο προφίλ ICC του, οι δείκτες του καταλόγου, η επικύρωση, ζει στο ίδιο το PDFiumPas, σε μια μονάδα καθαρού Pascal περίπου 2.000 γραμμών (FPdfPdfa.pas) που αναλύει τα αποθηκευμένα bytes και τα ξαναγράφει μέσω μιας αυξητικής ενημέρωσης. Το να ξέρετε πού γίνεται η δουλειά σάς λέει πού κρύβονται τα σφάλματα, και δεν κρύβονται στο PDFium

Τι απαιτεί πραγματικά το PDF/A και πού δημιουργεί πρόβλημα

Το PDF/A δεν είναι ένα μόνο format. Το ISO 19005 ορίζει τρία μέρη (PDF/A-1, -2, -3) και, μέσα σε κάθε ένα, επίπεδα συμμόρφωσης που υπόσχονται διαφορετικά πράγματα. Το Επίπεδο B (basic) εγγυάται μόνο ότι η οπτική εμφάνιση μπορεί να αναπαραχθεί. Το Επίπεδο A (accessible) προσθέτει ένα δέντρο δομημένου περιεχομένου με ετικέτες και αντιστοίχιση Unicode πάνω από το B. Το Επίπεδο U, που υπάρχει μόνο για τα μέρη 2 και 3, βρίσκεται ανάμεσά τους: αξιόπιστο κείμενο Unicode χωρίς το πλήρες δέντρο δομής. Το ISO 19005-1 δεν έχει Επίπεδο U, ένας περιορισμός που η βιβλιοθήκη τον κωδικοποιεί άμεσα

Μερικοί από τους κανόνες της μορφής είναι αυτοί που σε χτυπούν στην πράξη. Η κρυπτογράφηση απαγορεύεται εντελώς (ISO 19005-1 §6.1.3 και οι διάδοχοί του): ένα αρχείο PDF/A δεν μπορεί να φέρει ένα /Encrypt λεξικό Encryption. Το έγγραφο πρέπει να δηλώσει μια συνθήκη απόδοσης μέσω ενός OutputIntent του οποίου ο προορισμός είναι έγκυρο προφίλ ICC (§6.2.3.2). Η ίδια η δήλωση συμμόρφωσης πρέπει να εμφανίζεται ως μεταδεδομένα XMP κάτω από το σχήμα αναγνώρισης PDF/A. Το Επίπεδο A απαιτεί επιπλέον τη λογική δομή του §6.8, το δέντρο ετικετών που κάνει το έγγραφο αναγνώσιμο από μηχανή. Παραλείψτε οποιοδήποτε από αυτά και ένας ελεγκτής συμμόρφωσης απορρίπτει το αρχείο ακόμη κι αν αποδίδεται τέλεια

Η μία κλήση που παράγει ένα αρχείο

Το PDFiumPas εκθέτει ολόκληρη τη ροή πίσω από TPdf.SaveAsPdfA. Η απλή υπερφόρτωση δέχεται μια στοχευμένη συμμόρφωση και ως προεπιλογή χρησιμοποιεί PDF/A-1b, που είναι η σωστή προεπιλογή για τη συνηθισμένη περίπτωση του «κάνε αυτό να αποδίδεται για πάντα»

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Κάτω από το καπό, αυτή είναι μια κίνηση δύο σταδίων. SaveAsPdfA πρώτα ζητά από το PDFium να σειριοποιήσει το έγγραφο με FPDF_SaveAsCopy, έπειτα παραδίδει εκείνη τη ροή bytes στο InjectPdfAMarkers, το οποίο προσθέτει τα μεταδεδομένα XMP, το OutputIntent sRGB με το ενσωματωμένο προφίλ ICC και έναν αναθεωρημένο κατάλογο ως αυξητική ενημέρωση. Η πηγή διαβάζεται από τη θέση μηδέν και ο προορισμός γράφεται από τη θέση μηδέν· το αρχικό δέντρο αντικειμένων μένει άθικτο και οι δείκτες έρχονται μετά τα υπάρχοντα %%EOF. Αν χρειάζεστε τα bytes αντί για αρχείο, SaveAsPdfAToStream δέχεται ένα TStream και τις ίδιες επιλογές

Επιλογή της συμμόρφωσης με το record επιλογών

Για να στοχεύσετε ένα συγκεκριμένο μέρος και επίπεδο, περάστε ένα TPdfASaveOptions record. Το Conformance πεδίο του δέχεται ένα TPdfAConformance τιμή. Η απαρίθμηση καλύπτει κάθε έγκυρο συνδυασμό και τίποτε άλλο: pac1b, pac1a για το μέρος 1; pac2b, pac2u, pac2a για το μέρος 2; pac3b, pac3u, pac3a για το μέρος 3, συν pacUnknown και pacNone για την πλευρά της επικύρωσης. Δεν υπάρχει pac1u, επειδή αυτό το επίπεδο δεν υπάρχει στο πρότυπο

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Το μεγαλύτερο μέρος του record μπορεί να παραμείνει κενό. Αφήστε Title, Author, Subject, Keywords, Creator, και Producer αφήστε τα κενά και SaveAsPdfA τα συμπληρώνει αυτόματα από το λεξικό Info του εγγράφου μέσω FPDF_GetMetaText. Αφήστε το CreationDate και το ModDate κενά και χρησιμοποιεί την τρέχουσα ώρα UTC και για τις δύο ημερομηνίες XMP. Αφήστε το DocumentId και το InstanceId κενά και η βιβλιοθήκη τα προγεμίζει από το FPDF_GetFileIdentifier, με εναλλακτική ένα ντετερμινιστικό ID που παράγεται από τα bytes προέλευσης. Το μόνο πεδίο που ίσως θέλετε να παρακάμψετε σκόπιμα είναι το IccProfileData: το κενό σημαίνει το ενσωματωμένο προφίλ sRGB IEC61966-2.1, αλλά μια ροή εργασίας CMYK ή κλίμακας του γκρι πρέπει να δώσει το δικό της

Γιατί το Level A υποβαθμίζεται και γιατί αυτό είναι η ειλικρινής επιλογή

Υπάρχει εδώ μια λεπτομέρεια που εκπλήσσει όσους περιμένουν ότι ένας δείκτης θα είναι εγγύηση. Μπορείτε να ζητήσετε pac1a σε ένα έγγραφο που δεν έχει δέντρο ετικετών, αλλά το PDF/A-1a απαιτεί λογική δομή §6.8, και η βιβλιοθήκη δεν μπορεί να κατασκευάσει δέντρο δομής από ένα PDF χωρίς ετικέτες. Αντί να εκδώσει ένα αρχείο που δηλώνει Level A ενώ αποτυγχάνει σε αυτό, SaveAsPdfA ελέγχει αν υπάρχει πραγματική επισημασμένη δομή (/StructTreeRoot συν /MarkInfo με /Marked true) και, αν λείπει, υποβαθμίζει τη δήλωση: το pac1a γίνεται pac1b, το pac2a γίνεται pac2b, και ούτω καθεξής και για τα τρία μέρη. Τα εσωτερικά βοηθητικά στοιχεία είναι το PdfAIsLevelA και το PdfADowngradeToLevelB

Το σκεπτικό αξίζει να το πούμε ξεκάθαρα: ένα αρχείο που δηλώνει ειλικρινά το επίπεδο που καλύπτει είναι πιο χρήσιμο από ένα που λέει ψέματα για ένα επίπεδο που δεν καλύπτει. Το Level U χειρίζεται διαφορετικά. Η ανίχνευση πραγματικής κάλυψης Unicode θα σήμαινε έναν αφελή έλεγχο «έχει /ToUnicode» που θα υποβάθμιζε υπερβολικά νόμιμα έγγραφα (το WinAnsi και παρόμοιες κωδικοποιήσεις εξαιρούνται), οπότε η πλευρά αποθήκευσης εκδίδει τη δήλωση U όπως την όρισε ο καλών και αφήνει τη διαφορά να επισημανθεί στην πλευρά επικύρωσης. Αν χρειάζεστε ένα αρχείο Level A με εγγύηση, επισημάνετε το έγγραφο πριν το μετατρέψετε, ο μετατροπέας δεν θα εφεύρει δομή που δεν υπάρχει

Η παγίδα ICC που εντοπίζει μόνο ένας πραγματικός επικυρωτής

Αυτή είναι η αποτυχία που δίδαξε το πιο δύσκολο μάθημα, επειδή ο δικός της ελεγκτής της βιβλιοθήκης την πέρασε, ενώ το veraPDF, ο έλεγχος αναφοράς του ISO 19005, όχι. Το PDF/A απαιτεί το προφίλ προορισμού του OutputIntent να είναι έγκυρο ICCBased stream, και η §6.2.3.2 υποχρεώνει έναν επαληθευτή να ελέγχει αυτό το stream ως χρωματικό χώρο. Ένα ICCBased stream πρέπει να δηλώνει /N, τον αριθμό των χρωματικών συνιστωσών. Μια πρώιμη έκδοση του injector έγραφε το λεξικό του ICC stream μόνο με /Length και χωρίς /N, και το veraPDF απέρριψε το αποτέλεσμα με "The N entry (value null)... is missing"

Αυτό που το έκανε ύπουλο είναι ότι η απόρριψη εμφανιζόταν μόνο για PDF/A-1b και -1a. Τα μοντέλα συμμόρφωσης των μερών 2 και 3 δεν εκτελούσαν αυτόν τον συγκεκριμένο έλεγχο στο προφίλ προορισμού, οπότε η ίδια εισαχθείσα δομή περνούσε με pac2b, pac3b και pac2u αλλά αποτύγχανε με το pac1b μόνο και μόνο λόγω της τιμής pdfaid:part του. Ένα unit test δεν θα μπορούσε ποτέ να το δει, επειδή το ενσωματωμένο ValidatePdfACompliance ελέγχει μόνο ότι το /DestOutputProfile κλειδί υπήρχε, όχι τι βρισκόταν μέσα στο λεξικό ροής. Τα εσωτερικά tests πέρασαν, αλλά η πραγματική επαλήθευση αρχειοθέτησης απέτυχε

Η διόρθωση είναι IccComponentCount, η οποία διαβάζει την υπογραφή του χρωματικού χώρου των δεδομένων στο offset 16 της κεφαλίδας ICC και τη χαρτογραφεί σε πλήθος συνιστωσών: GRAY είναι 1, RGB , Lab , και XYZ είναι 3, CMYK είναι 4, με ένα άγνωστο προφίλ να προεπιλέγεται σε 3. Αυτό το πλήθος μπαίνει στο stream dictionary ως /N. Υπολογίζεται και δεν είναι hard-coded σε 3, ώστε ένας καλών που δίνει προφίλ CMYK ή grayscale μέσω IccProfileData να παίρνει τη σωστή τιμή. Το ευρύτερο μάθημα είναι μεθοδολογικό: ο εσωτερικός ελεγκτής της βιβλιοθήκης και ένας authoritative validator έχουν ο καθένας τα τυφλά τους σημεία, και η έξοδος PDF/A πρέπει να δοκιμάζεται end to end απέναντι σε μια reference implementation όπως το veraPDF αντί να εμπιστεύεται σε self-checks. Η ίδια πειθαρχία των incremental updates πίσω από τα καθαρά archives καλύπτεται στο επαλήθευση συμπιεσμένων object και xref streams, κάτι που έχει σημασία επειδή τα σύγχρονα PDFs που καταναλώνει ο injector βασίζονται συχνά σε cross-reference streams

Κρυπτογράφηση, xref streams και άλλες οριακές περιπτώσεις

Επειδή το ISO 19005 απαγορεύει την κρυπτογράφηση, η διαδρομή αποθήκευσης την αφαιρεί πριν γράψει. SaveAsPdfA εφαρμόζεται FPDF_REMOVE_SECURITY κατά τη σειριοποίηση, έτσι ώστε μια κρυπτογραφημένη πηγή (που φορτώθηκε με τον κωδικό πρόσβασής της) να αποκρυπτογραφείται κατά τη μεταφορά της στο archive. Σε ένα μη κρυπτογραφημένο έγγραφο αυτό είναι no-op και δεν αλλάζει τίποτα. Το αντίστοιχο συμπέρασμα είναι ο ίδιος περιορισμός που επιβάλλει το HotPDF από την άλλη πλευρά: ένα μόνο αρχείο δεν μπορεί να είναι ταυτόχρονα κρυπτογραφημένο και PDF/A. Όταν μια ροή εργασίας χρειάζεται και τα δύο, η λύση είναι δύο παραγόμενα αρχεία, ένα κρυπτογραφημένο αντίγραφο για διανομή και ένα ξεχωριστό καθαρό αντίγραφο για το archive

Μια ακόμη οριακή περίπτωση είναι αόρατη μέχρι να εμφανιστεί το πρόβλημα: έγγραφα PDF 1.5+ που χρησιμοποιούν καθαρό cross-reference stream και δεν φέρουν το trailer keyword. Ο injector διαβάζει το trailer για να βρει το source /Info και να προσαρτήσει το incremental update του, και πρέπει να δέχεται τη μορφή xref-stream, αλλιώς ένα τέτοιο έγγραφο θα αντιγραφόταν με τα markers να χάνονται σιωπηρά. Το ISO 32000-1 §7.5.6 επιτρέπει ρητά σε ένα κλασικό trailer incremental update να ακολουθεί ένα έγγραφο xref-stream, με /Prev να δείχνει στο offset του xref-stream, που είναι ακριβώς η δομή που εκπέμπει ο injector. Το ίδιο το FPDF_SaveAsCopy του PDFium γράφει πάντα ένα κλασικό trailer, οπότε στο κανονικό pipeline ο injector δεν συναντά ποτέ καθαρή πηγή xref-stream, αλλά η read path το χειρίζεται για έγγραφα που έρχονται από αλλού

Επαλήθευση πριν εμπιστευτείτε τον ισχυρισμό

Η βιβλιοθήκη συνοδεύεται από έναν byte-level checker, TPdf.ValidatePdfA, ο οποίος επιστρέφει ένα TPdfAValidationResult.Conformance πεδίο του αναφέρει το ανιχνευμένο επίπεδο και το Issues είναι ένα σύνολο από TPdfAValidationIssue τιμές· η βοηθητική μέθοδος IsCompliant είναι αληθής μόνο όταν ανιχνεύτηκε πραγματικό επίπεδο και το σύνολο ζητημάτων είναι κενό. Τρέξτε το ως γρήγορο πρώτο φίλτρο σε μια παρτίδα

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

Να είστε ειλικρινείς για το τι σας προσφέρει αυτό. Ο byte-level checker εντοπίζει δομικά προβλήματα (ένα λείπει OutputIntent, μια απαγορευμένη ενέργεια, ένα παρόν /Encrypt, διαφάνεια εκεί όπου το μέρος 1 την απαγορεύει) με υψηλή βεβαιότητα, και η ανίχνευση ενσωμάτωσης γραμματοσειρών χρησιμοποιεί ένα heuristic μέτρησης που δηλώνει επίτηδες μόνο ένα σήμα υψηλής βεβαιότητας αντί να κυνηγά κάλυψη ανά glyph. Αυτό που δεν κάνει είναι ανάλυση operators του content stream, κάτι που θα απαιτούσε έναν πλήρη content parser και είναι εκτός scope by design. Για ένα release gate, συνδυάστε τον in-library checker με το veraPDF: ο checker είναι στιγμιαίος και τρέχει παντού χωρίς DLL, το veraPDF είναι authoritative. Η ενσωμάτωση αυτού του ζεύγους σε μια batch εκτέλεση είναι το θέμα του CLI προελέγχου παρτίδας, που είναι το σημείο όπου ανήκει αυτή η επαλήθευση σε μια πραγματική ροή εργασίας αρχειοθέτησης

Το SaveAsPdfA, InjectPdfAMarkers και ValidatePdfA APIs που φαίνονται εδώ συνοδεύονται από το PDFium Component για Delphi, C++Builder και Lazarus/FPC. Η σελίδα προϊόντος συνδέει την πλήρη αναφορά API, συμπεριλαμβανομένης της πλήρους απαρίθμησης συμμόρφωσης και του record επιλογών πίσω από αυτά τα παραδείγματα