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

Ανίχνευση και Εξαγωγή Φορμών XFA στο Delphi με το PDFium

Ναι — το PDFium υποστηρίζει φόρμες XFA, με έναν σημαντικό διαχωρισμό. Το PDFium Component μπορεί να ανιχνεύσει μια φόρμα XFA και να εξαγάγετε τα πακέτα της template, datasets και config ως ακατέργαστο XML χρησιμοποιώντας την τυπική DLL, με μηδενική εξάρτηση χρόνου εκτέλεσης. Η εκτέλεση μιας δυναμικής φόρμας XFA — η διάταξη της XML περιγραφής της σε ζωντανές, διαδραστικές σελίδες — είναι ένα ξεχωριστό πρόβλημα που επιπλέον απαιτεί την έκδοση με ενεργοποιημένο το V8. Η κοινότητα συνεχίζει να ρωτά αν το PDFium \"κάνει XFA\" σαν να πρόκειται για μια απλή ικανότητα ναι-ή-όχι, και η ειλικρινής απάντηση είναι ότι η ανάγνωση δεδομένων XFA και η απόδοση μιας φόρμας XFA είναι δύο πολύ διαφορετικά πράγματα με πολύ διαφορετικά αποτυπώματα εξαρτήσεων

Αυτό το άρθρο εξετάζει και τα δύο μισά με το PDFium Component, το εγγενές VCL wrapper γύρω από το PDFium για Delphi και C++Builder. Πρώτα τη διαδρομή ανίχνευσης και στατικής εξαγωγής — FormType, την ιδιότητα XFA και τους αναγνώστες GetXfaPacket* — στη συνέχεια τη δυναμική διαδρομή χρόνου εκτέλεσης με τον περιορισμό του V8, και τέλος τις δοκιμές δυνατοτήτων (capability probes) που σας επιτρέπουν να αποτυγχάνετε κομψά όταν μια αναπτυγμένη DLL δεν μπορεί να εκτελέσει τη μηχανή

Υποστηρίζει το PDFium φόρμες XFA;

Δείτε το ως δύο δυνατότητες, όχι ως μία. Η στατική εξαγωγή XFA είναι πάντα διαθέσιμη: οι χαμηλού επιπέδου εξαγωγές FPDF_GetXFAPacket* που υποστηρίζουν τα GetXfaPacketCount, GetXfaPacketName και GetXfaPacketContent είναι μεταγλωττισμένες σε κάθε λογικά σύγχρονη pdfium.dll, ακόμη και σε εκδόσεις με απενεργοποιημένο το XFA. Αυτός είναι ο λόγος για τον οποίο η εξαγωγή των ακατέργαστων XML template, datasets και config από μια φόρμα δεν φέρει καμία απολύτως εξάρτηση χρόνου εκτέλεσης — είναι καθαρά δομική ανάγνωση, χωρίς τη συμμετοχή της μηχανής. Η δυναμική XFA είναι η άλλη δυνατότητα: η πραγματική εκτέλεση της φόρμας, η αξιολόγηση των υπολογισμών της και η σελιδοποίηση από τη μηχανή διάταξης XFA. Αυτή η διαδρομή υπάρχει μόνο σε εκδόσεις του PDFium που έχουν μεταγλωττιστεί με υποστήριξη XFA πάνω από τη μηχανή V8 JavaScript, και είναι το θέμα για το οποίο διαφωνούν οι περισσότερες συζητήσεις σχετικά με το αν το PDFium υποστηρίζει XFA

Τα πρότυπα πλαισιώνουν καθαρά αυτόν το διαχωρισμό. Το XFA ορίζεται εκτός της κύριας γραμματικής του PDF στο Adobe XFA 3.3 (2012), και το PDF 1.7 §8.6.7 περιγράφει πώς μεταφέρεται μια φόρμα XFA μέσα σε ένα PDF: τα πακέτα της φόρμας ζουν κάτω από την καταχώριση /XFA του λεξικού AcroForm, και ένα έγγραφο που πρέπει να διαμορφωθεί από έναν επεξεργαστή XFA προτού σημάνει οτιδήποτε ορίζει το /NeedsRendering true στον κατάλογο (catalog). Αυτοί οι δύο δείκτες είναι ακριβώς αυτό που αναζητά το PDFium Component, και ακριβώς αυτό στο οποίο μπορείτε να βασιστείτε όταν συλλογίζεστε για ένα αρχείο που δεν έχετε ξαναδεί

Πώς διαφέρει το XFA από μια AcroForm;

Μια AcroForm είναι η κλασική φόρμα PDF: σχολιασμοί γραφικών στοιχείων (widgets) — πεδία κειμένου, πλαίσια επιλογής, ομάδες ραδιοπλήκτρων — αγκυροβολημένα σε σταθερές συντεταγμένες σε συνηθισμένες σελίδες PDF. Η σελίδα που βλέπετε είναι η σελίδα στο αρχείο, και τα πεδία κάθονται πάνω της. Το XFA ακολουθεί την αντίθετη προσέγγιση. Η διαδραστική φόρμα περιγράφεται εξ ολοκλήρου σε XML, και σε ένα πλήρες (δυναμικό) έγγραφο XFA οι σελίδες PDF είναι ουσιαστικά ένας placeholder — το πραγματικό περιεχόμενο δημιουργείται κατά το άνοιγμα από μια μηχανή XFA που διαβάζει το πρότυπο XML και το διατάσσει, αυξανόμενο ή συρρικνούμενο για να ταιριάξει στα δεδομένα. Αυτό είναι που ανακοινώνει το /NeedsRendering true: χωρίς επεξεργαστή XFA, οι στατικές σελίδες μπορεί να είναι κενές ή να φέρουν μόνο μια ειδοποίηση "παρακαλούμε ανοίξτε σε συμβατό πρόγραμμα προβολής"

Το ίδιο το XFA payload είναι ένα σύνολο από επώνυμα πακέτα, και τρία από αυτά μεταφέρουν όλα όσα χρειάζονται οι περισσότερες ενσωματώσεις. Το πακέτο template είναι ο ορισμός της φόρμας — πεδία, διάταξη, υπολογισμοί και σενάρια. Το πακέτο datasets κρατά τα πραγματικά δεδομένα της παρουσίας (instance data), τις συμπληρωμένες τιμές των πεδίων ως δέντρο XML. Το πακέτο config μεταφέρει οδηγίες επεξεργασίας για τη μηχανή XFA. Υπάρχουν και άλλα — localeSet, connectionSet, το xdp wrapper — αλλά για τον έλεγχο μιας φόρμας ή τη μετανάστευση των δεδομένων από αυτήν, το template συν το datasets είναι συνήθως όλη η δουλειά. Επειδή αυτά τα δεδομένα είναι απλώς XML που κάθεται σε μια ροή (stream), η ανάγνωσή τους δεν απαιτεί ποτέ την εκτέλεση τίποτα, που είναι ο βασικός λόγος για τον οποίο η στατική εξαγωγή δεν έχει εξαρτήσεις

Ανίχνευση φόρμας XFA στο Delphi

Η ανίχνευση ξεκινά με τον τρόπο που κάνει κάθε εργασία του PDFium Component: ορίστε το FileName, γυρίστε το Active := True και ελέγξτε αν άνοιξε πραγματικά. Το Active := True δεν προκαλεί ποτέ σφάλμα — ένα αρχείο που λείπει, λάθος κωδικός ή απούσα DLL αφήνει απλώς το Active σε False, οπότε ο έλεγχος είναι υποχρεωτικός. Μόλις ανοίξει το έγγραφο, το FormType σας λέει ποιο μοντέλο φόρμας χρησιμοποιεί. Δύο από τις τιμές του TPdfFormType είναι XFA: το ftXfaFull είναι μια πλήρης, δυναμική φόρμα XFA, αυτή που χρειάζεται απόδοση, ενώ το ftXfaForeground είναι η XFAF, το προσκήνιο υποσύνολο όπου το περιεχόμενο XFA σχεδιάζεται πάνω από κατά τα άλλα κανονικές σελίδες AcroForm, ώστε οι στατικές σελίδες να παραμένουν ουσιαστικές. Το boolean XFA είναι μια συντόμευση για το "αυτό είναι ένα έγγραφο XFA οποιασδήποτε μορφής". Εάν εργάζεστε επίσης με τα πεδία επιπέδου γραφικού στοιχείου (widgets) σε ένα έγγραφο XFAF ή AcroForm, οι μηχανισμοί καλύπτονται στον οδηγό πλοήγησης πεδίων φόρμας PDFium

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // κατεστραμμένο, κρυπτογραφημένο ή λείπει η DLL
    case Pdf.FormType of
      ftXfaFull:
        Log('Full (dynamic) XFA form');
      ftXfaForeground:
        Log('XFAF: XFA layered over static AcroForm pages');
      ftAcroForm:
        Log('Classic AcroForm');
    else
      Log('No interactive form');
    end;
    if Pdf.XFA then
      Log('Document carries an XFA packet payload');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Εξαγωγή πακέτων XFA ως ακατέργαστο XML

Η εξαγωγή είναι το εξαιρετικά χρήσιμο μέρος χωρίς εξαρτήσεις. Προστατεύστε την κλήση με το XfaStaticReadable — επιβεβαιώνει ότι οι εξαγωγές του packet reader επιλύθηκαν στη φορτωμένη DLL — και στη συνέχεια διατρέξτε τα πακέτα κατά ευρετήριο. Το GetXfaPacketCount επιστρέφει πόσα πακέτα μεταφέρει η φόρμα, το GetXfaPacketName δίνει στο καθένα το όνομά του, και το GetXfaPacketContent επιστρέφει τα ακατέργαστα bytes, τα οποία για τα γνωστά πακέτα είναι UTF-8 XML. Όταν γνωρίζετε ήδη ποιο πακέτο θέλετε, οι επώνυμες βοηθητικές μέθοδοι GetXfaTemplate, GetXfaDatasets και GetXfaConfig επιλύουν το όνομα για εσάς και επιστρέφουν TBytes απευθείας. Για μια εργασία μετανάστευσης δεδομένων, αυτό που έχει σημασία είναι το GetXfaDatasets: σας δίνει τις υποβληθείσες τιμές πεδίων ως έγγραφο XML που μπορείτε να αναλύσετε και να αντιστοιχίσετε στο δικό σας σχήμα, το ίδιο είδος εργασίας που θα κάνατε μετά την εξαγωγή κειμένου από ένα PDF

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // οι εξαγωγές του packet reader δεν είναι παρούσες
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // ακατέργαστο UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Ή μεταβείτε απευθείας στις συμπληρωμένες τιμές πεδίων με το όνομα:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

Γιατί η δυναμική απόδοση XFA απαιτεί τη μηχανή V8;

Συμπέρασμα πρώτα: επειδή μια δυναμική φόρμα XFA είναι ένα μικρό πρόγραμμα, και όχι ένα στατικό σχέδιο. Το πακέτο template μεταφέρει υπολογισμούς FormCalc και JavaScript, επικυρώσεις και σενάρια συμβάντων (event scripts), και η μηχανή διάταξης XFA πρέπει να τα εκτελέσει για να αποφασίσει πώς θα μοιάζουν οι σελίδες. Το PDFium υλοποιεί αυτήν τη μηχανή μόνο σε εκδόσεις που έχουν μεταγλωττιστεί με υποστήριξη XFA πάνω από το V8, τη μηχανή JavaScript της Google — ο ίδιος λόγος για τον οποίο η DLL με V8 (pdfium32v8.dll ή pdfium64v8.dll) είναι αισθητά μεγαλύτερη από την τυπική έκδοση. Χαι χωρίς το V8 δεν υπάρχει κανείς να εκτελέσει τα σενάρια, οπότε δεν υπάρχουν αποδοθείσες σελίδες για εμφάνιση, παρά μόνο το ακατέργαστο XML που μπορείτε ήδη να διαβάσετε στατικά

Η επιλογή αυτής της έκδοσης έχει έναν σκληρό περιορισμό που δεν μπορείτε να παρακάμψετε: είναι μια απόφαση μία φορά ανά διεργασία. Μια μεμονωμένη διεργασία δεν μπορεί να φορτώσει τόσο την τυπική pdfium.dll όσο και την έκδοση V8 — εξάγουν τα ίδια σύμβολα και το V8 διατηρεί καθολική κατάσταση isolate — οπότε το PDFium Component πρέπει να επιλέξει την έκδοση V8 προτού η βιβλιοθήκη φορτωθεί για πρώτη φορά. Για να γίνει αυτό αυτόματα, το LoadDocument προ-σαρώνει το αρχείο για τους δείκτες /XFA και /NeedsRendering true (ο ίδιος έλεγχος που εκτίθεται ως η αυτόνομη συνάρτηση PdfFileLooksXfa) και ορίζει το EnableV8Engine := True πριν από τη φόρτωση όταν τους βρίσκει. Μόλις μια απλή pdfium.dll είναι ήδη παρούσα στη μνήμη, η αλλαγή δεν μπορεί πλέον να τεθεί σε ισχύ, και αυτή η περίπτωση είναι που αναφέρει η διαδρομή του runtime-missing

begin
  // Κοιτάξτε για XFA προτού το PDFium φορτωθεί για πρώτη φορά, ώστε η έκδοση με V8 να μπορεί ακόμα να επιλεγεί. Η PdfFileLooksXfa σαρώνει για /XFA και /NeedsRendering true χωρίς να αναλύει ολόκληρο το αρχείο.
  if PdfFileLooksXfa('dynamic-form.pdf') then
    EnableV8Engine := True;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'dynamic-form.pdf';
    Pdf.Active := True;
    if Pdf.Active and Pdf.XFA then
    begin
      if Pdf.XfaRuntimeAvailable then
        RenderXfaPages(Pdf)          // η μηχανή είναι ενεργή: δυναμική διάταξη
      else
        ExtractXfaData(Pdf, 'C:\out'); // επαναφορά σε στατικά πακέτα
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Κομψός χειρισμός του XFA runtime που λείπει

Τρεις δοκιμές (probes) σας λένε ακριβώς πόση XFA δυνατότητα έχετε, και ο ειλικρινής κώδικας τις ελέγχει αντί να υποθέτει. Το XfaFeaturesAvailable είναι ένας καθολικός έλεγχος ότι οι βοηθητικές εξαγωγές XFA/V8 είναι παρούσες στη φορτωμένη DLL. Το XfaStaticReadable επιβεβαιώνει, ανά έγγραφο, ότι οι packet readers επιλύθηκαν — η προστασία για την εξαγωγή. Το XfaRuntimeAvailable είναι το ισχυρό: είναι αληθές μόνο όταν αυτό το συγκεκριμένο έγγραφο ενεργοποίησε πράγματι τη μηχανή XFA, πράγμα που σημαίνει ότι είναι XFA, η έκδοση V8 φορτώθηκε και η FPDF_LoadXFA πέτυχε. Ένα έγγραφο μπορεί να αναφέρει XFA = True αλλά XfaRuntimeAvailable = False όποτε η αναπτυγμένη DLL στερείται XFA ή η τυπική έκδοση είχε ήδη φορτωθεί

Όταν ένα έγγραφο XFA ανοίγει αλλά η μηχανή δεν μπορεί να εκτελεστεί, το PDFium Component δεν ρισκάρει. Κλειδώνει τη φόρμα στην ασφαλή στατική λειτουργία αντί να ζητήσει ένα δυναμικό runtime που δεν μπορεί να υποστηρίξει, και εγείρει το OnXfaRuntimeMissing μια φορά ώστε ο κεντρικός υπολογιστής να μπορεί να αντιδράσει — συνήθως λέγοντας στον χρήστη να επανεκκινήσει με την έκδοση με V8, ενώ εξακολουθεί να προσφέρει τα στατικώς αναγνώσιμα δεδομένα στο μεταξύ. Η σύνδεση αυτού του χειριστή (handler) είναι η διαφορά μεταξύ μιας καθαρής επαναφοράς και μιας φόρμας που σιωπηλά δεν δείχνει τίποτα

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Ενεργοποιείται μία φορά όταν ένα έγγραφο XFA ανοίγει αλλά η μηχανή δεν μπορεί να εκτελεστεί: μια απλή pdfium.dll ήταν ήδη φορτωμένη, ή η έκδοση στερείται XFA/V8.
  ShowMessage('This is a dynamic XFA form. Restart with the ' +
    'V8-enabled build to render it; its packet data is still readable.');
end;

Να είστε ειλικρινείς σχετικά με αυτό το όριο και στο δικό σας προϊόν. Εάν δεν στέλνετε ποτέ τη DLL με V8, οι δυναμικές φόρμες XFA δεν θα αποδίδονται ποτέ για τους χρήστες σας — αλλά μπορείτε ακόμα να τις ανιχνεύσετε, να εξαγάγετε κάθε πακέτο και να τραβήξετε τα υποβληθέντα δεδομένα, πράγμα που αρκεί για τη μεγάλη κατηγορία εργασιών που αφορούν πραγματικά τη λήψη δεδομένων από παλαιού τύπου κυβερνητικές και τραπεζικές φόρμες αντί για τη διαδραστική παρουσίασή τους. Κρατήστε την πιο βαριά έκδοση V8 για τις περιπτώσεις που χρειάζονται πραγματικά μια ζωντανή, συμπληρώσιμη φόρμα, και αφήστε τις δοκιμές δυνατοτήτων να δρομολογήσουν κάθε έγγραφο στη διαδρομή που μπορεί πραγματικά να ακολουθήσει. Εάν η ροή εξαγωγής σας χειρίζεται επίσης ενσωματωμένα αρχεία, η ίδια φιλοσοφία μόνο για ανάγνωση ισχύει για τα συνημμένα PDF στο Delphi

Τα API FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig και οι δοκιμές δυνατοτήτων που εμφανίζονται εδώ αποτελούν μέρος του PDFium Component για Delphi και C++Builder