PDFlibPas, la bibliothèque PDF Delphi et C++Builder native, donne à un document PDF deux endroits séparés pour accrocher un comportement automatique : des actions de cycle de vie au niveau document telles que WillClose, WillSave, DidSave, WillPrint et DidPrint, stockées dans le dictionnaire /AA du Catalog, et des actions de cycle de vie au niveau page — Open et Close — stockées à la place dans le propre dictionnaire /AA de chaque objet Page. Confondre les deux conteneurs est la façon la plus courante dont une action de cycle de vie ne fait silencieusement rien
Les cas motivants sont ordinaires. Une équipe finance veut un modèle de relevé qui estampille un horodatage d'impression et journalise qui l'a imprimé au moment où l'impression démarre réellement, pas quand le fichier s'ouvre simplement. Un flux de travail riche en formulaires a besoin que les valeurs de champ soient poussées automatiquement vers un serveur avant que le client PDF du lecteur ne soit autorisé à fermer la fenêtre, afin qu'un onglet fermé ne signifie jamais une modification perdue. Un rapport multi-pages veut une bannière spécifique à une page qui n'apparaît que pendant que cette page est à l'écran. PDF offre en réalité un troisième niveau en dessous de document et page pour ce genre de comportement — des actions attachées à la propre entrée /A d'un champ de formulaire individuel ou d'un lien, le sujet d'un article compagnon sur les actions de formulaire interactives et JavaScript — mais cet article reste aux deux niveaux au-dessus : tout le document, et une seule page
Quels déclencheurs vivent sur le /AA du Catalog de document ?
Cinq déclencheurs vivent sur le dictionnaire /AA du Catalog, et chacun d'eux se déclenche pour un événement qui affecte tout le document, pas une seule page. ISO 32000-1 §12.6.3 (Trigger Events) liste les clés au niveau document comme WC, WS, DS, WP et DP — les noms littéraux à deux lettres écrits dans le dictionnaire /AA — pour WillClose, WillSave, DidSave, WillPrint et DidPrint respectivement, et PDFlibPas reflète exactement cet ensemble dans l'énumération TPDFlibDocumentActionTrigger : datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction est le point d'entrée unique qui attache l'une des cinq, et le paramètre ActionKind qu'elle prend est l'une des dix constantes PDF_ACTION_BUILDER_* partagées à travers chaque appel de constructeur d'action dans la bibliothèque, d'un simple URI à un script à un saut de destination. Ce que fait réellement une action GoTo, fichier distant, fichier intégré ou Launch une fois déclenchée est une question différente de où elle est attachée, et c'est le sujet d'un article compagnon sur les actions GoTo, distantes, intégrées et de lancement — celui-ci reste sur la question du conteneur, Catalog ou Page, plutôt que sur la question du type d'action
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;
En quoi un déclencheur au niveau page diffère-t-il d'un déclencheur au niveau document ?
Un déclencheur au niveau page ne se déclenche que pour le seul objet Page auquel il est attaché, et PDFlibPas le stocke dans le propre dictionnaire /AA de cette page plutôt que dans celui du Catalog. Il n'y a que deux déclencheurs de page, Open et Close, correspondant aux clés O et C qu'ISO 32000-1 définit pour le dictionnaire d'actions supplémentaires d'une page, et PDFlibPas les expose comme patOpen et patClose via SetPageAction, qui s'attache à quelle que soit la page actuellement sélectionnée via SelectPage — un détail qui compte la première fois que vous bouclez à travers un document en vous attendant à ce qu'un appel s'applique partout, car ce n'est jamais le cas. Attacher l'un ou l'autre type de déclencheur élève aussi la version PDF minimale du fichier, et les deux conteneurs demandent des planchers différents : PDFlibPas élève le document à au moins PDF 1.4 la première fois qu'il écrit une entrée /AA de Catalog, et à au moins PDF 1.5 la première fois qu'il écrit une entrée /AA de Page, quel que soit le type d'action à l'intérieur. C'est une exigence au niveau conteneur superposée à ce dont l'action elle-même a besoin par elle-même, si bien qu'une simple action URI qui n'exigerait que PDF 1.1 à elle seule tire tout de même tout le fichier jusqu'à PDF 1.5 une fois enveloppée dans un déclencheur d'ouverture de page
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);
Lire et supprimer des actions de cycle de vie
GetDocumentActionInfo et GetPageActionInfo renvoient tous deux un enregistrement TPDFlibActionInfo, et le champ Kind revient akNone chaque fois que ce déclencheur n'a rien d'attaché, donc vérifiez Kind avant de faire confiance à tout autre champ de l'enregistrement — URI, JavaScript, FileName et le reste ne sont significatifs que pour le seul type d'action que Kind rapporte réellement, puisque la même forme d'enregistrement est réutilisée à travers chaque type d'action que le constructeur peut produire. RemoveDocumentAction et RemovePageAction effacent chacun un seul déclencheur et rapportent 1 quand ils ont trouvé quelque chose à supprimer, 0 quand le déclencheur était déjà vide ; quand l'entrée supprimée était la dernière restante dans le dictionnaire /AA, PDFlibPas supprime le /AA désormais vide lui-même plutôt que de laisser un conteneur pendant, sans signification, derrière sur le Catalog ou la page
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 permet-il les actions de cycle de vie du tout ?
Non. La conformité PDF/A rejette tout le conteneur d'actions supplémentaires, pas seulement les types d'action qui paraissent risqués, car ISO 19005 restreint le modèle d'action interactive de PDF sur l'hypothèse qu'un fichier d'archive doit se rendre de la même façon des décennies plus tard, sans dépendre d'un moteur de script ou d'une connexion réseau qui pourrait ne plus exister d'ici là. SetLifecycleAction, le constructeur partagé derrière à la fois SetDocumentAction et SetPageAction, vérifie PDFAMode avant même de regarder ActionKind, si bien qu'une action URI qui ouvre simplement une page web d'entreprise ou une action Named qui ne signifie que aller à la page suivante se retrouve prise dans le même filet qu'une dangereuse — rien qu'un relecteur de sécurité signalerait normalement, bloqué tout de même, car la restriction est structurelle plutôt qu'au cas par cas. Le danger pratique est que le rejet est silencieux : SetDocumentAction et SetPageAction renvoient tous deux 0 sans lever d'exception, si bien qu'un point d'appel qui ne vérifie jamais la valeur de retour livre un document manquant silencieusement le déclencheur qu'il était censé porter
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');
Une asymétrie mérite d'être gardée à l'esprit. RemoveDocumentAction et RemovePageAction ne vérifient jamais PDFAMode, si bien que charger un fichier qui porte déjà des actions de cycle de vie non conformes et les retirer en chemin vers un enregistrement conforme PDF/A fonctionne exactement comme prévu — seul le chemin d'écriture, attacher un nouveau déclencheur, est conditionné par le mode de conformité
Où l'impression à l'ouverture s'intègre-t-elle sans déclencheur WillOpen ?
Le dictionnaire /AA du Catalog n'a aucune entrée WillOpen du tout, par conception — le /AA au niveau document dans ISO 32000-1 définit exactement cinq clés, WillClose, WillSave, DidSave, WillPrint et DidPrint, et rien dans cette liste ne se déclenche purement parce qu'un fichier a été ouvert. Le crochet au moment de l'ouverture vit dans une entrée de Catalog séparée, /OpenAction, que PDFlibPas expose via sa propre famille d'appels, SetOpenActionJavaScript, SetOpenActionDestination et SetOpenActionNamedDestination parmi eux, dont aucun ne touche du tout au dictionnaire /AA ni à l'énumération TPDFlibDocumentActionTrigger. Les deux mécanismes se composent bien, cependant, et c'est habituellement ce dont un modèle d'impression-à-l'ouverture a réellement besoin : construire le modèle afin que son /OpenAction démarre la tâche d'impression, typiquement une action JavaScript appelant la propre commande d'impression de la visionneuse, et l'impression elle-même est ce qui donne à WillPrint et DidPrint quelque chose contre quoi s'exécuter — un horodatage estampillé avant que les pages ne soient mises en file, une entrée d'audit écrite une fois qu'elles sont terminées
Quelle est la fiabilité de ces déclencheurs à travers les visionneuses PDF ?
Toutes les visionneuses ne les exécutent pas, même en dehors de PDF/A, donc traitez une action de cycle de vie comme une demande plutôt qu'une garantie. Acrobat et la plupart des lecteurs de bureau complets exécutent fidèlement l'ensemble complet, mais une grande part de la consommation PDF du monde réel ne touche jamais du tout à un dictionnaire d'actions supplémentaires : les visionneuses intégrées aux navigateurs, la plupart des lecteurs mobiles, et presque tous les pipelines de rendu côté serveur ou d'extraction de texte ignorent purement et simplement /AA ou n'en honorent qu'une tranche étroite, WillPrint et DidPrint se portant généralement le plus mal puisque la conversion sans interface n'a aucune opération d'impression à laquelle s'accrocher. Si une action de soumission de formulaire WillClose est le seul chemin qui capture les données de formulaire, ce n'est pas un chemin fiable — associez-la à un bouton de soumission explicite, et traitez le déclencheur automatique comme une commodité pour les lecteurs qui se trouvent le prendre en charge
Les déclencheurs de document, de page et de champ sont trois niveaux de la même mécanique de dictionnaire d'action sous-jacente, et une fois le conteneur clair, le reste consiste à choisir la bonne constante ActionKind et à vérifier le code de retour. Ces déclencheurs de cycle de vie, ainsi que l'API de constructeur d'action plus large que cet article aborde, font partie de la bibliothèque PDF Delphi PDFlibPas standard, avec la référence complète des déclencheurs et types d'action dans la documentation produit