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

Ισοπέδωση συνδέσμων εμπλουτισμένου κειμένου XFA σε συνδέσμους PDF στο Delphi

Η XFA (XML Forms Architecture) είναι καταργημένη. Το ISO 32000-1 τη φέρει στην παράγραφο §12.7 με τη σημείωση ότι αφαιρείται από το PDF 2.0, και οι σύγχρονοι προβολείς (viewers) εγκαταλείπουν τις μηχανές XFA τους μία προς μία. Τίποτα από αυτά δεν άδειασε τα αρχεία. Κυβερνητικές φόρμες εισαγωγής, ασφαλιστικές αιτήσεις και τραπεζικές καταστάσεις συντάχθηκαν ως XFA για το μεγαλύτερο μέρος των δύο δεκαετιών, και αυτά τα αρχεία εξακολουθούν να φτάνουν στα εισερχόμενα και στις ροές εγγράφων σήμερα. Όταν ο προβολέας που συνήθιζε να τα αποδίδει σταματήσει να το κάνει, η φόρμα μετατρέπεται σε μια κενή σελίδα με ένα σύμβολο κράτησης θέσης "παρακαλώ ανοίξτε σε διαφορετικό αναγνώστη". Η ανθεκτική λύση είναι να ισοπεδώσετε (flatten) την XFA σε στατικό περιεχόμενο PDF που μπορεί να ζωγραφίσει οποιοσδήποτε αναγνώστης

Το δύσκολο κομμάτι αυτής της ισοπέδωσης δεν είναι τα πεδία. Τα πλαίσια κειμένου (text boxes) και τα πλαίσια ελέγχου (check boxes) αντιστοιχίζονται στα widgets του AcroForm αρκετά καθαρά. Το δύσκολο κομμάτι είναι το εμπλουτισμένο κείμενο που αποθηκεύει η XFA μέσα σε ένα στοιχείο draw, σε ένα μπλοκ <exData contentType="text/html">. Αυτό το μπλοκ είναι ένα υποσύνολο HTML με ενσωματωμένο στυλ (inline styling) και, συχνά, άγκυρες (anchors). Η μεταφορά του στη σελίδα σημαίνει αναπαραγωγή τόσο του στυλιζαρισμένου κειμένου όσο και των ζωντανών υπερσυνδέσμων, και οι υπερσύνδεσμοι είναι το σημείο όπου οι περισσότερες υλοποιήσεις τα παρατάνε αθόρυβα

Πώς μοιάζει πραγματικά το εμπλουτισμένο κείμενο XFA

Ένα σώμα exData είναι μια μικρή φέτα XHTML. Μια παράγραφος είναι ένα <p>. ένα στυλιζαρισμένο τμήμα χαρακτήρων (span) είναι ένα <span> με το δικό του ενσωματωμένο CSS για βάρος, στάση, χρώμα και μέγεθος. και ένας υπερσύνδεσμος είναι ένα <a href="..."> που τυλίγει το ορατό κείμενό του. Μια μεμονωμένη γραμμή μπορεί να κρατήσει αρκετά spans στη σειρά, το καθένα με διαφορετικό στυλ, και ένα από αυτά μπορεί να είναι μια άγκυρα. Το στυλ δεν είναι διακόσμηση που μπορεί να παραλειφθεί. Μια ρήτρα που αποδίδεται με έντονο κόκκινο επειδή είναι νομική προειδοποίηση πρέπει να παραμείνει έντονη και κόκκινη μετά την ισοπέδωση, αλλιώς το ισοπεδωμένο έγγραφο παραποιεί το πρωτότυπο

Επομένως, η μηχανή ισοπέδωσης δεν μπορεί να αντιμετωπίσει το μπλοκ ως μία συμβολοσειρά. Πρέπει να διατρέξει την ενσωματωμένη δομή, να επιλύσει το αποτελεσματικό στυλ κάθε διαδοχής (run) τοποθετώντας το ενσωματωμένο CSS του span πάνω από τη βασική γραμματοσειρά του στοιχείου draw, και να τοποθετήσει τις διαδοχές τη μία μετά την άλλη κατά μήκος της γραμμής. Το HotPDF μοντελοποιεί καθένα από αυτά τα τοποθετημένα τμήματα ως μια εσωτερική εγγραφή TXFARichRun. Η εγγραφή φέρει το κείμενο της διαδοχής, το επιλυμένο στυλ της, το μετρημένο κουτί της και, για μια άγκυρα, το Href στο οποίο δείχνει

Τοποθέτηση των διαδοχών από αριστερά προς τα δεξιά

Η τοποθέτηση είναι το σημείο όπου το εμπλουτισμένο κείμενο σταματά να είναι πρόβλημα ανάλυσης και γίνεται πρόβλημα στοιχειοθεσίας. Οι διαδοχές μοιράζονται μια γραμμή, οπότε κάθε διαδοχή ξεκινά εκεί που τελείωσε η προηγούμενη. Δεν υπάρχει σήμανση (markup) που να καταγράφει αυτές τις θέσεις. πρέπει να μετρηθούν. Η εσωτερική ρουτίνα LayoutRichText της μηχανής μετράει κάθε διαδοχή με τις ίδιες μετρικές γραμματοσειράς που αργότερα θα τη ζωγραφίσουν, έπειτα ορίζει την οριζόντια μετατόπιση της διαδοχής στο τρέχον άθροισμα όλων των προηγούμενων πλατών των διαδοχών. Η διαδοχή ένα ξεκινά από την αρχή του κουτιού draw, η διαδοχή δύο ξεκινά από το πλάτος της διαδοχής ένα, η διαδοχή τρία στο συνδυασμένο πλάτος των δύο πρώτων, και ούτω καθεξής κατά μήκος της γραμμής

Αυτός είναι ο λόγος για τον οποίο η ευθυγράμμιση της γραμματοσειράς μέτρησης έχει τόση σημασία. Το πέρασμα της διάταξης μετράει τις προόδους. ένα ξεχωριστό πέρασμα απόδοσης σχεδιάζει τα γλυφικά. Εάν αυτά τα δύο περάσματα διαφωνούν σχετικά με τη γραμματοσειρά, τα κουτιά που υπολόγισε η διάταξη δεν θα καθίσουν κάτω από τα γλυφικά που ζωγραφίζει ο αποδότης (renderer). Το HotPDF τα κρατά σε συγχρονισμό αντιστοιχίζοντας το επιλυμένο στυλ κάθε διαδοχής σε μια προδιαγραφή γραμματοσειράς, μέσω του εσωτερικού βοηθού RunStyleToFontSpec, η οποία ταιριάζει με τις προεπιλογές του ίδιου του αποδότη του Arial σε 10 στιγμές. Η μετρημένη πρόοδος και το σχεδιασμένο κείμενο τότε συμφωνούν, και το υπολογισμένο κουτί μιας διαδοχής καλύπτει πραγματικά τους χαρακτήρες που βλέπει ο αναγνώστης

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

Από μια διαδοχή άγκυρας σε έναν σχολιασμό PDF Link

Ένας υπερσύνδεσμος σε ένα τελικό PDF δεν αποτελεί μέρος του περιεχομένου της σελίδας. Είναι ένα ξεχωριστό αντικείμενο, ένας σχολιασμός Link, που περιγράφεται στο ISO 32000-1 §12.5.6.5. Ο σχολιασμός έχει ένα /Rect που ορίζει το ορθογώνιο που μπορεί να γίνει κλικ στη σελίδα και μια ενέργεια που ενεργοποιείται όταν γίνεται κλικ στο ορθογώνιο. Για έναν εξωτερικό σύνδεσμο, η ενέργεια είναι μια ενέργεια URI: /S /URI με τη διεύθυνση προορισμού ως τη συμβολοσειρά /URI του. Το ορατό κείμενο από κάτω είναι συνηθισμένο περιεχόμενο σελίδας. ο σχολιασμός είναι η αόρατη ενεργή ζώνη (hot zone) που τοποθετείται πάνω του

Η διαδρομή ισοπέδωσης ακολουθεί ακριβώς αυτό το μοντέλο. Όταν μια διαδοχή φέρει ένα Href, το HotPDF σχεδιάζει πρώτα το στυλιζαρισμένο κείμενο και μετά χτίζει έναν σχολιασμό Link πάνω από το κουτί της διαδοχής. Το δημόσιο σημείο εισόδου για αυτόν τον σχολιασμό είναι η μέθοδος της σελίδας AddURILink, η οποία δημιουργεί το αντικείμενο /Type /Annot /Subtype /Link με μια ενέργεια /URI και επιστρέφει το λεξικό σχολιασμού. Το ορθογώνιό του είναι το μετρημένο κουτί της διαδοχής, μεταφρασμένο από τις τοπικές συντεταγμένες του στοιχείου draw σε συντεταγμένες σελίδας. Το αποτέλεσμα είναι ένας σύνδεσμος που προσγειώνεται ακριβώς πάνω στο κείμενο της άγκυρας και πουθενά αλλού

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

Γιατί το κουτί χτυπήματος (hit box) πρέπει να προέρχεται από μετρημένα πλάτη

Είναι δελεαστικό να φανταστεί κανείς τον εντοπισμό του συνδέσμου ψάχνοντας στη σελίδα για το ορατό κείμενό του και σχεδιάζοντας το ορθογώνιο γύρω από οτιδήποτε βρεθεί. Αυτό δεν λειτουργεί, και ο λόγος είναι θεμελιώδης για το πώς αποθηκεύεται το ισοπεδωμένο κείμενο. Οι στυλιζαρισμένες διαδοχές ζωγραφίζονται με ενσωματωμένες υποσύνολο γραμματοσειρές (subset fonts). Μια γραμματοσειρά υποσυνόλου αναριθμεί τα γλυφικά που κρατά, οπότε η ροή περιεχομένου της σελίδας (page content stream) περιέχει δεκαεξαδικούς κωδικούς CID, όχι τους αρχικούς κωδικούς χαρακτήρων. Τα bytes στη σελίδα δεν είναι τα γράμματα που διαβάζει ένας άνθρωπος, και δεν μπορούν να αναζητηθούν ως κείμενο. Μια αναζήτηση για τη λεζάντα της άγκυρας δεν βρίσκει τίποτα, επειδή αυτή η λεζάντα δεν υπάρχει ως κυριολεκτικό κείμενο πουθενά στη ροή

Η μόνη αξιόπιστη άγκυρα για το ορθογώνιο είναι η γεωμετρία που το πέρασμα της διάταξης παρήγαγε ήδη. Η μετατόπιση και το μετρημένο πλάτος κάθε διαδοχής υπολογίστηκαν κατά τη ροή της γραμμής, πριν από την αναρίθμηση οποιουδήποτε γλυφικού, και περιγράφουν πού θα εμφανιστεί φυσικά το κείμενο. Συνεπώς, το HotPDF παίρνει το ορθογώνιο του συνδέσμου κατευθείαν από το τοποθετημένο κουτί της διαδοχής αντί από οποιαδήποτε αναζήτηση κειμένου. Επειδή η μέτρηση χρησιμοποίησε τη γραμματοσειρά απόδοσης, το κουτί είναι σωστό ανεξάρτητα από τη δημιουργία υποσυνόλου. Η γεωμετρία επιβιώνει από την κωδικοποίηση. το κείμενο όχι. Αυτό είναι όλο το επιχείρημα για την τοποθέτηση μετρημένου πλάτους, και αυτός είναι ο λόγος για τον οποίο ένας flattener που προσπαθεί να τοποθετήσει συνδέσμους εκ των υστέρων με αναζήτηση κειμένου παράγει ζώνες χτυπήματος που παρασύρονται ή εξαφανίζονται

Οδηγώντας την ισοπέδωση από τον κώδικά σας

Για ένα PDF που περιέχει ήδη ένα πακέτο XFA, το σημείο εισόδου είναι το FlattenLoadedXFA. Φορτώστε το έγγραφο, καλέστε τη μέθοδο και αποθηκεύστε το αποτέλεσμα. Η παράμετρος Editable αποφασίζει τι θα συμβεί στα πεδία της φόρμας: περάστε True για να τα διατηρήσετε ως συμπληρώσιμα widgets του AcroForm, ή False για να επισημάνετε κάθε widget ως μόνο για ανάγνωση, ώστε η έξοδος να είναι ένα παγωμένο αρχείο. Τα μπλοκ σχεδίασης εμπλουτισμένου κειμένου, με τις στυλιζαρισμένες διαδοχές και τους σχολιασμούς συνδέσμων τους, παράγονται σε κάθε περίπτωση. Η συνάρτηση επιστρέφει το πλήθος των widgets που εξέπεμψε

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

Να διαβάζετε πάντα το XFAFlattenWarnings μετά την κλήση. Η λίστα καθαρίζεται στην αρχή κάθε ισοπέδωσης και συγκεντρώνει μια γραμμή για κάθε στοιχείο που η μηχανή αρνήθηκε να αποδώσει: ένα μη υποστηριζόμενο είδος πεδίου, μια εικόνα draw που δεν αποκωδικοποιείται, ένα μπλοκ exData χωρίς χρησιμοποιήσιμα spans. Κανένα από αυτά δεν εγείρει εξαίρεση (exception), επομένως μια κενή λίστα προειδοποιήσεων είναι η απόδειξή σας ότι όλα αντιστοιχίστηκαν, και μια μη κενή σας λέει ακριβώς ποια πρωτότυπα πρέπει να επιθεωρήσετε. Όταν κρατάτε το ακατέργαστο XFA ως bytes XDP αντί για φορτωμένο PDF, η αδελφή μέθοδος ApplyXFAAsAcroForm παίρνει αυτά τα bytes απευθείας και μοιράζεται την ίδια διαδρομή κώδικα και την ίδια συμπεριφορά προειδοποιήσεων. Η συμπληρωματική μέθοδος AddXFAPacket ακολουθεί τον αντίθετο δρόμο, ενσωματώνοντας ένα πακέτο XFA σε ένα έγγραφο που κατασκευάζετε

Επιβεβαίωση του αποτελέσματος σε έναν αναγνώστη

Ανοίξτε το ισοπεδωμένο αρχείο στο Acrobat, ή σε οποιονδήποτε τρέχοντα προβολέα, και ελέγξτε δύο πράγματα. Πρώτον, ότι το εμπλουτισμένο κείμενο αποδόθηκε με το στυλ του ανέπαφο: οι έντονες διαδοχές είναι έντονες, οι έγχρωμες διαδοχές φέρουν το χρώμα τους, και τα spans κάθονται στη σωστή σειρά στη γραμμή αντί να αλληλοκαλύπτονται ή να τρέχουν έξω από το κουτί. Δεύτερον, οι υπερσύνδεσμοι είναι ζωντανοί. Περάστε το ποντίκι πάνω από μια άγκυρα και η γραμμή κατάστασης θα πρέπει να δείχνει τη διεύθυνση προορισμού. κάντε κλικ σε αυτήν και η ενέργεια URI θα πρέπει να την ανοίξει. Χρησιμοποιήστε τον επιθεωρητή σχολιασμών του προβολέα για να επιβεβαιώσετε ότι καθένας είναι ένας γνήσιος σχολιασμός /Link του οποίου το /Rect αγκαλιάζει το κείμενο της άγκυρας, καθισμένος πάνω σε περιεχόμενο που είναι πλέον απλά ζωγραφισμένα γλυφικά αντί για XFA αποδοσμένο από φόρμα. Αυτός ο συνδυασμός, στυλιζαρισμένο στατικό κείμενο συν πραγματικοί σχολιασμοί Link στα σωστά ορθογώνια, είναι αυτό που κάνει το ισοπεδωμένο έγγραφο να επιζήσει από τις μηχανές XFA που δεν χρειάζεται πλέον

Η ισοπέδωση των ίδιων των πεδίων, των πλαισίων κειμένου, των πλαισίων ελέγχου και των λιστών επιλογής που περιβάλλουν αυτό το εμπλουτισμένο κείμενο, καλύπτεται στο άρθρο μας σχετικά με την ισοπέδωση των φορμών XFA σε widgets του AcroForm. Για την ευρύτερη ιστορία της κατασκευής και της τοποθέτησης σχολιασμών Link με το χέρι, πέρα από αυτούς που παράγει η διαδρομή ισοπέδωσης, δείτε πώς να εργαστείτε με σχολιασμούς PDF στο HotPDF. Και τα δύο χτίζονται πάνω στο ίδιο μοντέλο σχολιασμών και φορμών που αποστέλλεται με το HotPDF Component για Delphi και C++Builder