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

Ισοπέδωση συνδέσμων εμπλουτισμένου κειμένου 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 στιγμές. Η μετρημένη πρόοδος και το σχεδιασμένο κείμενο τότε συμφωνούν, και το υπολογισμένο κουτί μιας διαδοχής καλύπτει πραγματικά τους χαρακτήρες που βλέπει ο αναγνώστης

Διάγραμμα του HotPDF να επιπεδώνει ένα μπλοκ rich-text exData XFA σε Delphi σε στυλιζαρισμένες εκτελέσεις τοποθετημένες από αριστερά προς δεξιά με μετρημένα πλάτη, όπου η εκτέλεση άγκυρας κρατά το href της και κάθε θραύσμα γίνεται εγγραφή TXFARichRun
Η μηχανή flatten διασχίζει την inline δομή exData, επιλύει το στυλ κάθε τμήματος, και μετράει πλάτος με τη γραμματοσειρά απόδοσης ώστε τα διατεταγμένα κουτιά να κάθονται ακριβώς κάτω από τους ζωγραφισμένους γλυφούς
// Εννοιολογικό σχήμα μιας διαδοχής (run) μετά τη στοιχειοθέτηση. Η μηχανή χτίζει μια συστοιχία από αυτές
// εσωτερικά· δεν τις κατασκευάζετε εσείς, αλλά τα πεδία εξηγούν πώς
// το κουτί χτυπήματος ενός συνδέσμου προκύπτει από τη μετρημένη γεωμετρία και όχι από το κείμενο.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // πάνω-αριστερά, σε σχέση με την αρχή του κουτιού σχεδίασης
    W, H   : Double;       // μετρημένο κουτί της διαδοχής (πλάτος από το πέρασμα διάταξης)
    Text   : AnsiString;   // οι ορατοί χαρακτήρες της διαδοχής
    Href   : AnsiString;   // στόχος URI για ένα run <a>, '' διαφορετικά
  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 σε συντεταγμένες σελίδας. Το αποτέλεσμα είναι ένας σύνδεσμος που προσγειώνεται ακριβώς πάνω στο κείμενο της άγκυρας και πουθενά αλλού

Διάγραμμα της διαδρομής επιπέδωσης HotPDF που μεταφράζει το μετρημένο πλαίσιο draw-local μιας εκτέλεσης άγκυρας σε συντεταγμένες σελίδας και εκπέμπει σχολιασμό Subtype Link με ενέργεια URI του οποίου το Rect αγκαλιάζει το κείμενο άγκυρας
Ένα τμήμα αγκύλης γίνεται ζωγραφισμένο κείμενο συν σχολιασμός Link του οποίου το /Rect είναι το μετρημένο κουτί του τμήματος μεταφρασμένο σε συντεταγμένες σελίδας, με την ενέργεια URI δημιουργημένη από την AddURILink
// Το ίδιο δημόσιο API που χρησιμοποιεί η διαδρομή ισοπέδωσης για κάθε άγκυρα. Παράγει
// έναν σχολιασμό Link κατά ISO 32000-1 12.5.6.5: /Subtype /Link με ενέργεια /URI
// πάνω στο δεδομένο ορθογώνιο. Η προαιρετική περιγραφή συμπληρώνει το /Contents, ώστε
// μια εφαρμογή ανάγνωσης οθόνης να ανακοινώνει τον προορισμό.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // κουτί χτυπήματος σε συντεταγμένες σελίδας για τη διαδοχή
  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 που προσπαθεί να τοποθετήσει συνδέσμους εκ των υστέρων με αναζήτηση κειμένου παράγει ζώνες χτυπήματος που παρασύρονται ή εξαφανίζονται

Διάγραμμα που δείχνει γιατί το HotPDF παίρνει τα hit boxes συνδέσμων XFA από τη μετρημένη γεωμετρία εκτελέσεων: τα υποσύνολα γραμματοσειρών επαναριθμούν glyphs σε κωδικούς CID ώστε η αναζήτηση κειμένου δεν βρίσκει τίποτα, ενώ οι μετατοπίσεις και τα πλάτη του περάσματος διάταξης επιβιώνουν και δίνουν το σωστό Rect
Ενσωματωμένες γραμματοσειρές υποσυνόλου επαναριθμούν γλυφούς σε κωδικούς CID, οπότε η αναζήτηση κειμένου δεν βρίσκει τίποτα· οι μετρημένες μετατοπίσεις και τα πλάτη του περάσματος διάταξης είναι οι μόνοι άγκυρες που επιβιώνουν της κωδικοποίησης

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

Για ένα 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 διατηρεί τα πεδία συμπληρώσιμα· το False τα παγώνει ως μόνο για ανάγνωση.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Οτιδήποτε δεν μπόρεσε να αντιστοιχίσει η μηχανή αναφέρεται, δεν εγείρεται ως εξαίρεση.
    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 Delphi Component για Delphi και C++Builder