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

Ενέργειες Κύκλου Ζωής PDF: Catalog /AA έναντι Page /AA σε Delphi

Το PDFlibPas, η εγγενής βιβλιοθήκη PDF για Delphi και C++Builder, δίνει σε ένα έγγραφο PDF δύο ξεχωριστά σημεία για να κρεμάσει αυτόματη συμπεριφορά: ενέργειες κύκλου ζωής σε επίπεδο εγγράφου όπως οι WillClose, WillSave, DidSave, WillPrint και DidPrint, αποθηκευμένες στο λεξικό /AA του Catalog, και ενέργειες κύκλου ζωής σε επίπεδο σελίδας — Open και Close — αποθηκευμένες αντ' αυτού στο δικό του λεξικό /AA κάθε αντικειμένου Page. Η σύγχυση των δύο containers είναι ο πιο συνηθισμένος τρόπος με τον οποίο μια ενέργεια κύκλου ζωής δεν κάνει σιωπηλά τίποτα

Οι περιπτώσεις που το κινητοποιούν είναι συνηθισμένες. Μια ομάδα οικονομικών θέλει ένα πρότυπο κατάστασης που σφραγίζει μια χρονοσφραγίδα εκτύπωσης και καταγράφει ποιος την εκτύπωσε τη στιγμή που πράγματι ξεκινά η εκτύπωση, όχι όταν το αρχείο απλώς ανοίγει. Μια ροή εργασίας με πολλές φόρμες χρειάζεται τιμές πεδίων να προωθούνται αυτόματα σε έναν διακομιστή πριν επιτραπεί στον πελάτη PDF του αναγνώστη να κλείσει το παράθυρο, ώστε μια κλειστή καρτέλα να μη σημαίνει ποτέ χαμένη επεξεργασία. Μια αναφορά πολλών σελίδων θέλει ένα banner ειδικό για μια σελίδα που εμφανίζεται μόνο ενώ εκείνη η σελίδα είναι στην οθόνη. Το PDF προσφέρει στην πραγματικότητα ένα τρίτο επίπεδο κάτω από το έγγραφο και τη σελίδα για αυτό το είδος συμπεριφοράς — ενέργειες προσαρτημένες στη δική της καταχώριση /A ενός μεμονωμένου πεδίου φόρμας ή συνδέσμου, το θέμα ενός συνοδευτικού άρθρου για διαδραστικές ενέργειες φόρμας και JavaScript — αλλά αυτό το άρθρο μένει στα δύο επίπεδα από πάνω: ολόκληρο το έγγραφο, και μία μόνη σελίδα

Ποιοι Ενεργοποιητές Ζουν στο /AA του Catalog Εγγράφου;

Πέντε ενεργοποιητές ζουν στο λεξικό /AA του Catalog, και καθένας τους πυροδοτείται για ένα συμβάν που επηρεάζει ολόκληρο το έγγραφο, όχι μία μόνη σελίδα. Το ISO 32000-1 §12.6.3 (Trigger Events) καταγράφει τα κλειδιά επιπέδου εγγράφου ως WC, WS, DS, WP και DP — τα κυριολεκτικά ονόματα δύο γραμμάτων που γράφονται στο λεξικό /AA — για τα WillClose, WillSave, DidSave, WillPrint και DidPrint αντίστοιχα, και το PDFlibPas αντικατοπτρίζει ακριβώς αυτό το σύνολο στην απαρίθμηση TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. Η SetDocumentAction είναι το μοναδικό σημείο εισόδου που προσαρτά οποιαδήποτε από τις πέντε, και η παράμετρος ActionKind που δέχεται είναι μία από δέκα σταθερές PDF_ACTION_BUILDER_* κοινές σε κάθε κλήση action-builder της βιβλιοθήκης, από ένα απλό URI έως ένα script έως ένα άλμα προορισμού. Τι πραγματικά κάνει μια ενέργεια GoTo, remote-file, embedded-file ή Launch μόλις πυροδοτηθεί είναι διαφορετικό ερώτημα από το πού προσαρτάται, και αυτό είναι το θέμα ενός συνοδευτικού άρθρου για ενέργειες GoTo, remote, embedded και launch — αυτό εδώ μένει στο ερώτημα του container, Catalog ή Page, παρά στο ερώτημα του είδους ενέργειας

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.AddStandardFont(4);
    Lib.DrawText(40, 700, 'Quarterly statement');
    Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
      'https://example.com/audit/will-save', '', 0, 0);
    Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
      'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
    Lib.SaveToFile('statement.pdf');
  finally
    Lib.Free;
  end;
end;

Πώς Διαφέρει ένας Ενεργοποιητής Επιπέδου Σελίδας από έναν Επιπέδου Εγγράφου;

Ένας ενεργοποιητής επιπέδου σελίδας πυροδοτείται μόνο για το μοναδικό αντικείμενο Page στο οποίο είναι προσαρτημένος, και το PDFlibPas τον αποθηκεύει στο δικό του λεξικό /AA εκείνης της σελίδας αντί σε αυτό του Catalog. Υπάρχουν μόνο δύο ενεργοποιητές σελίδας, Open και Close, που αντιστοιχούν στα κλειδιά O και C που ορίζει το ISO 32000-1 για το λεξικό επιπρόσθετων ενεργειών μιας σελίδας, και το PDFlibPas τους εκθέτει ως patOpen και patClose μέσω της SetPageAction, η οποία προσαρτάται σε όποια σελίδα είναι τρέχουσα επιλεγμένη μέσω της SelectPage — μια λεπτομέρεια που έχει σημασία την πρώτη φορά που κάνετε βρόχο σε ένα έγγραφο περιμένοντας μία κλήση να ισχύσει παντού, γιατί ποτέ δεν ισχύει. Η προσάρτηση οποιουδήποτε από τα δύο είδη ενεργοποιητή ανεβάζει επίσης την ελάχιστη έκδοση PDF του αρχείου, και τα δύο containers ζητούν διαφορετικά κατώτατα όρια: το PDFlibPas ανεβάζει το έγγραφο σε τουλάχιστον PDF 1.4 την πρώτη φορά που γράφει μια καταχώριση /AA στο Catalog, και σε τουλάχιστον PDF 1.5 την πρώτη φορά που γράφει μια καταχώριση /AA στη σελίδα, ανεξάρτητα από το ποιο είδος ενέργειας βρίσκεται μέσα. Αυτή είναι μια απαίτηση σε επίπεδο container επιπλέον σε ό,τι χρειάζεται η ίδια η ενέργεια από μόνη της, οπότε μια γυμνή ενέργεια URI που θα απαιτούσε μόνο PDF 1.1 μόνη της εξακολουθεί να τραβά ολόκληρο το αρχείο στο PDF 1.5 μόλις τυλιχτεί σε έναν ενεργοποιητή ανοίγματος σελίδας

Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
  'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
  'https://example.com/analytics/page-3-closed', '', 0, 0);

Ανάγνωση και Αφαίρεση Ενεργειών Κύκλου Ζωής

Οι GetDocumentActionInfo και GetPageActionInfo επιστρέφουν και οι δύο μια εγγραφή TPDFlibActionInfo, και το πεδίο Kind επιστρέφει akNone όποτε εκείνος ο ενεργοποιητής δεν έχει τίποτα προσαρτημένο, οπότε ελέγξτε το Kind πριν εμπιστευτείτε οποιοδήποτε άλλο πεδίο στην εγγραφή — τα URI, JavaScript, FileName και τα υπόλοιπα έχουν νόημα μόνο για το ένα είδος ενέργειας που πράγματι αναφέρει το Kind, αφού το ίδιο σχήμα εγγραφής επαναχρησιμοποιείται σε κάθε είδος ενέργειας που μπορεί να παράγει ο builder. Οι RemoveDocumentAction και RemovePageAction καθαρίζουν η καθεμία έναν μόνο ενεργοποιητή και αναφέρουν 1 όταν βρήκαν κάτι να αφαιρέσουν, 0 όταν ο ενεργοποιητής ήταν ήδη κενός· όταν η αφαιρεθείσα καταχώριση ήταν η τελευταία που είχε απομείνει στο λεξικό /AA, το PDFlibPas διαγράφει το ίδιο το πλέον κενό /AA αντί να αφήσει πίσω ένα εκκρεμές, χωρίς νόημα container στο Catalog ή στη σελίδα

var
  Info: TPDFlibActionInfo;
begin
  Info := Lib.GetDocumentActionInfo(datWillSave);
  if Info.Kind = akURI then
    WriteLn('WillSave calls out to: ', string(Info.URI));

  if Lib.RemoveDocumentAction(datWillSave) = 1 then
    Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
      'https://example.com/audit/will-save-v2', '', 0, 0);
end;

Επιτρέπει Καν το PDF/A Ενέργειες Κύκλου Ζωής;

Όχι. Η συμμόρφωση PDF/A απορρίπτει ολόκληρο το container επιπρόσθετων ενεργειών, όχι μόνο τα είδη ενεργειών που ακούγονται επικίνδυνα, γιατί το ISO 19005 περιορίζει το διαδραστικό μοντέλο ενεργειών του PDF με την υπόθεση ότι ένα αρχειακό αρχείο πρέπει να αποδίδεται με τον ίδιο τρόπο δεκαετίες από τώρα, χωρίς να εξαρτάται από μια μηχανή σεναρίων ή μια σύνδεση δικτύου που μπορεί να μην υπάρχει πια τότε. Η SetLifecycleAction, ο κοινός builder πίσω από τις SetDocumentAction και SetPageAction, ελέγχει το PDFAMode πριν καν κοιτάξει το ActionKind, οπότε μια ενέργεια URI που απλώς ανοίγει μια εταιρική ιστοσελίδα ή μια ενέργεια Named που απλώς σημαίνει πήγαινε στην επόμενη σελίδα πιάνεται στο ίδιο δίχτυ με μια επικίνδυνη, τίποτα που θα σημείωνε κανονικά ένας ελεγκτής ασφάλειας, μπλοκάρεται ούτως ή άλλως, γιατί ο περιορισμός είναι δομικός παρά κατά περίπτωση. Ο πρακτικός κίνδυνος είναι ότι η απόρριψη είναι σιωπηλή: οι SetDocumentAction και SetPageAction επιστρέφουν και οι δύο 0 χωρίς να εγείρουν εξαίρεση, οπότε ένα σημείο κλήσης που ποτέ δεν ελέγχει την τιμή επιστροφής παραδίδει ένα έγγραφο που λείπει σιωπηλά τον ενεργοποιητή που υποτίθεται ότι έπρεπε να φέρει

Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
     '', '', 0, 0) = 0 then
  // rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
  WriteLn('lifecycle action not attached');

Μια ασυμμετρία αξίζει να έχετε στο μυαλό σας. Οι RemoveDocumentAction και RemovePageAction ποτέ δεν ελέγχουν το PDFAMode, οπότε η φόρτωση ενός αρχείου που ήδη φέρει μη συμμορφούμενες ενέργειες κύκλου ζωής και η αφαίρεσή τους στην πορεία προς μια αποθήκευση συμμορφούμενη με PDF/A λειτουργεί ακριβώς όπως αναμένεται — μόνο η διαδρομή εγγραφής, η προσάρτηση νέου ενεργοποιητή, είναι φραγμένη στη λειτουργία συμμόρφωσης

Πού Ταιριάζει το Print-on-Open Χωρίς Ενεργοποιητή WillOpen;

Το λεξικό /AA του Catalog δεν έχει καθόλου καταχώριση WillOpen, εκ σχεδιασμού — το /AA επιπέδου εγγράφου στο ISO 32000-1 ορίζει ακριβώς πέντε κλειδιά, WillClose, WillSave, DidSave, WillPrint και DidPrint, και τίποτα σε εκείνη τη λίστα δεν πυροδοτείται καθαρά επειδή ένα αρχείο ανοίχτηκε. Το hook χρόνου ανοίγματος ζει σε μια ξεχωριστή καταχώριση Catalog, το /OpenAction, το οποίο το PDFlibPas εκθέτει μέσω της δικής του οικογένειας κλήσεων, SetOpenActionJavaScript, SetOpenActionDestination και SetOpenActionNamedDestination μεταξύ αυτών, καμία από τις οποίες δεν αγγίζει καθόλου το λεξικό /AA ή την απαρίθμηση TPDFlibDocumentActionTrigger. Οι δύο μηχανισμοί όμως συνθέτονται, και αυτό είναι συνήθως αυτό που χρειάζεται πράγματι ένα πρότυπο print-on-open: χτίστε το πρότυπο ώστε το δικό του /OpenAction να ξεκινά την εργασία εκτύπωσης, τυπικά μια ενέργεια JavaScript που καλεί την ίδια την εντολή εκτύπωσης του θεατή, και η ίδια η εκτύπωση είναι αυτή που δίνει στα WillPrint και DidPrint κάτι να εκτελέσουν πάνω — μια χρονοσφραγίδα σφραγισμένη πριν φύγουν οι σελίδες, μια εγγραφή ελέγχου γραμμένη μόλις τελειώσουν

Πόσο Αξιόπιστοι Είναι Αυτοί οι Ενεργοποιητές σε Διάφορους Θεατές PDF;

Δεν τους εκτελεί κάθε θεατής, ακόμα και εκτός PDF/A, οπότε αντιμετωπίστε μια ενέργεια κύκλου ζωής ως αίτημα παρά ως εγγύηση. Το Acrobat και οι περισσότεροι πλήρεις desktop αναγνώστες εκτελούν πιστά ολόκληρο το σύνολο, αλλά ένα μεγάλο μερίδιο πραγματικής κατανάλωσης PDF ποτέ δεν αγγίζει καθόλου ένα λεξικό επιπρόσθετων ενεργειών: θεατές ενσωματωμένοι σε browser, οι περισσότεροι αναγνώστες κινητών, και σχεδόν κάθε pipeline απόδοσης ή εξαγωγής κειμένου από την πλευρά του διακομιστή είτε αγνοούν εντελώς το /AA είτε τιμούν μόνο ένα στενό τμήμα του, με τα WillPrint και DidPrint τυπικά να τα πάνε χειρότερα αφού η μετατροπή χωρίς κεφαλή δεν έχει καμία λειτουργία εκτύπωσης για να συνδεθούν. Αν μια ενέργεια υποβολής φόρμας WillClose είναι η μόνη διαδρομή που καταγράφει δεδομένα φόρμας, δεν είναι αξιόπιστη διαδρομή — συνδυάστε την με ένα ρητό κουμπί υποβολής, και αντιμετωπίστε τον αυτόματο ενεργοποιητή ως ευκολία για τους αναγνώστες που τυχαίνει να την υποστηρίζουν

Οι ενεργοποιητές εγγράφου, σελίδας και πεδίου είναι τρία επίπεδα του ίδιου υποκείμενου μηχανισμού λεξικών ενεργειών, και μόλις το container ξεκαθαριστεί, το υπόλοιπο είναι να επιλέξετε τη σωστή σταθερά ActionKind και να ελέγξετε τον κωδικό επιστροφής. Αυτοί οι ενεργοποιητές κύκλου ζωής, μαζί με το ευρύτερο API action-builder που αγγίζει αυτό το άρθρο, αποστέλλονται ως μέρος της τυπικής βιβλιοθήκης PDF PDFlibPas για Delphi, με την πλήρη αναφορά ενεργοποιητών και ειδών ενέργειας στην τεκμηρίωση του προϊόντος