Technical Article

Μετατροπή XFA Rich-Text Hyperlinks σε PDF Links στο Delphi

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

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

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

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

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

Διάταξη των runs από αριστερά προς τα δεξιά

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

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

// 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;

Από ένα anchor run σε ένα PDF Link annotation

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

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

// 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 πρέπει να προέρχεται από μετρημένα πλάτη

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

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

Καθοδήγηση του flatten από τον κώδικά σας

Για ένα PDF που περιέχει ήδη ένα πακέτο XFA, το σημείο εισόδου είναι η FlattenLoadedXFA. Φορτώστε το έγγραφο, καλέστε τη μέθοδο και αποθηκεύστε το αποτέλεσμα. Η παράμετρος Editable αποφασίζει τι συμβαίνει με τα πεδία φόρμας: περάστε True για να τα διατηρήσετε ως συμπληρώσιμα widgets AcroForm, ή False για να επισημάνετε κάθε widget ως read-only, ώστε η έξοδος να είναι ένα παγωμένο αρχείο. Τα μπλοκ σχεδίασης πλούσιου κειμένου, με τα διαμορφωμένα runs τους και τις σημειώσεις συνδέσμων, παράγονται σε κάθε περίπτωση. Η συνάρτηση επιστρέφει το πλήθος των 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 μετά την κλήση. Η λίστα εκκαθαρίζεται στην έναρξη κάθε flatten και συγκεντρώνει μια γραμμή για κάθε στοιχείο που η μηχανή αρνήθηκε να αποδώσει: ένα μη υποστηριζόμενο είδος πεδίου, μια εικόνα σχεδίασης που δεν αποκωδικοποιείται, ένα μπλοκ exData χωρίς χρησιμοποιήσιμα spans. Κανένα από αυτά δεν εγείρει εξαίρεση, επομένως μια άδεια λίστα προειδοποιήσεων είναι η απόδειξή σας ότι όλα χαρτογραφήθηκαν, και μια μη άδεια σας λέει ακριβώς ποια πρωτότυπα πρέπει να επιθεωρήσετε. Όταν κρατάτε το ακατέργαστο XFA ως byte XDP αντί για φορτωμένο PDF, η αδελφή μέθοδος ApplyXFAAsAcroForm λαμβάνει αυτά τα byte απευθείας και μοιράζεται την ίδια διαδρομή κώδικα και την ίδια συμπεριφορά προειδοποιήσεων. Η συμπληρωματική μέθοδος AddXFAPacket ακολουθεί την αντίθετη κατεύθυνση, ενσωματώνει ένα πακέτο XFA σε ένα έγγραφο που κατασκευάζετε.

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

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

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