Article technique

Actions GoToR, GoToE, et Launch dans les PDF Delphi

PDFlibPas donne aux développeurs Delphi et C++Builder trois types d'action pour une navigation qui laisse la page actuelle derrière : GoToR (Go To Remote) ouvre une page spécifique dans un autre fichier PDF, GoToE (Go To Embedded) ouvre un fichier PDF intégré à l'intérieur du document actuel, et Launch exécute un programme externe ou ouvre un fichier via le shell du système d'exploitation. Les trois vivent dans ISO 32000-1 §12.6.4, la section Action Types qui définit aussi l'action GoTo quotidienne, et chacun porte son propre piège pour les non avertis : un numéro de page qui signifie quelque chose de différent selon quel appel le construit, une cible qui est un nom plutôt qu'un chemin de fichier, et une paire de paramètres de chaîne qui se ressemblent identiquement mais servent deux visionneuses différentes

Rien de tout cela n'est hypothétique. Un ensemble de référence technique — un manuel principal, un PDF de spécifications qu'un distributeur met à jour selon son propre calendrier, un utilitaire d'étalonnage installé aux côtés des deux — s'appuie précisément sur ce genre de câblage inter-documents : une référence croisée qui doit atterrir sur la page 5 du fichier de spécifications, une fiche technique qui vaut la peine d'être livrée à l'intérieur du manuel plutôt qu'à côté, un lien qui renvoie directement à l'outil d'étalonnage. Cet article est l'image miroir de relire les actions de signet et d'annotation depuis un PDF existant : cette pièce couvre la consommation d'une action GoToR, Launch, ou GoToE qu'un autre producteur a déjà écrite dans un fichier ; celle-ci couvre la construction de ces mêmes trois types d'action à partir de zéro, y compris les règles au niveau des champs que PDFlibPas applique avant de committer un seul octet

Trois façons pour une action PDF de quitter la page actuelle

PDFlibPas sépare la navigation locale de tout le reste à la clé /S de l'action, et GoToR, GoToE, et Launch sont les trois sous-types dont la cible se trouve en dehors de la page actuelle : GoToR sous ISO 32000-1 §12.6.4.3, GoToE sous §12.6.4.4, et Launch sous §12.6.4.5, tous à l'intérieur de la section plus large §12.6.4 Action Types qui définit aussi l'action GoTo quotidienne. La destination d'une simple action GoTo nomme un objet page qui existe déjà à l'intérieur du document, si bien que PDFlibPas peut la valider immédiatement ; GoToR et GoToE ne peuvent pas le faire de la même façon, puisque le fichier externe pourrait même ne pas exister sur cette machine et que le nombre de pages d'un fichier intégré n'est pas quelque chose que le document hôte suit, si bien que les deux portent une référence non résolue plutôt qu'un lien dur — une spécification de fichier plus une destination pour GoToR, un nom de fichier intégré plus une page cible pour GoToE — tandis que Launch abandonne entièrement le concept de destination et nomme simplement quelque chose que le système d'exploitation doit exécuter ou ouvrir. Cette séparation apparaît comme deux familles d'appel côté écriture : des constructeurs de haut niveau, en un coup, tels que AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF, et AddLinkToLocalFile créent ensemble une annotation de lien de zone de page et son action, couvrant la plupart des mises en page réelles — une ligne de texte ou une icône qu'un lecteur clique — tandis que des définisseurs de plus bas niveau tels que SetActionRemoteDestinationEx, SetActionLaunchOptions, et leurs homologues AddActionNext* attachent ou remplacent une action sur quelque chose dont vous détenez déjà un handle : un signet existant, un déclencheur de champ de formulaire, ou un événement de cycle de vie au niveau document ou page. Les deux familles finissent par écrire les mêmes formes de dictionnaire ; la différence est où vous vous trouvez quand vous les appelez, et, comme la section suivante le couvre, ce que signifie un numéro de page quand vous le faites

Comment construire un lien GoToR qui ouvre une page dans un autre fichier PDF ?

Une action GoToR a besoin de deux choses — une spécification de fichier et une destination à l'intérieur de ce fichier — et PDFlibPas expose deux appels différents pour fournir la seconde partie, chacun avec sa propre convention de numérotation de page. AddLinkToFile et AddLinkToFileEx, les constructeurs de zone de page de haut niveau, valident leur argument Page ou DestPage comme supérieur à zéro, la même numérotation basée sur 1 que PDFlibPas utilise partout ailleurs, y compris SelectPage. SetActionRemoteDestinationEx, le définisseur de plus bas niveau utilisé pour attacher ou remplacer une action GoToR sur quelque chose dont vous avez déjà un handle, valide plutôt DestPage comme supérieur ou égal à zéro et l'écrit directement dans le tableau de destination explicite de l'action sans aucun ajustement : il veut l'index de page brut, basé sur zéro, du document cible, la numérotation qu'ISO 32000-1 spécifie pour une destination explicite distante. Appelez le définisseur de bas niveau avec le même nombre que vous donneriez au constructeur de haut niveau et le lien ouvre une page trop tôt

var
  Lib: TPDFlib;
  ActionID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('manual.pdf', '') = 1 then
    begin
      Lib.SelectPage(12);
      // Page is 1-based here, same as SelectPage above: this opens
      // the fifth page of specs.pdf.
      Lib.AddLinkToFile(72, 700, 200, 16, 'specs.pdf', 5, 0, 0, 0);

      // A later maintenance pass repoints the same link at a
      // reorganized file. SetActionRemoteDestinationEx edits the
      // action directly, and DestPage here is the zero-based index
      // PDF itself uses for a remote explicit destination -- "the
      // fifth page" is now 4, not 5.
      ActionID := Lib.GetAnnotActionID(1);
      Lib.SetActionRemoteDestinationEx(ActionID, 'specs-2026.pdf',
        4, Ord(dkFit), 0, 0, 0, 0, 0, 0, -1);
    end;
  finally
    Lib.Free;
  end;
end;

Le reste des arguments de SetActionRemoteDestinationEx est tout aussi littéral. ValueMask est un ensemble de bits — 1 pour gauche, 2 pour haut, 4 pour droite, 8 pour bas, 16 pour zoom — et PDFlibPas le vérifie contre DestType avant d'écrire quoi que ce soit : une destination dkFitR doit fournir exactement 15 (les quatre bords, pas de zoom), dkFit et dkFitB doivent fournir 0, et dkFitH/dkFitV n'acceptent que leur seule coordonnée pertinente. Les bits que vous laissez non définis à l'intérieur d'un masque par ailleurs valide ne sont pas omis du tableau ; ils sont écrits comme un null PDF explicite, qu'ISO 32000-1 traite comme « garder quelle que soit la valeur que la visionneuse a déjà » pour cette coordonnée — une façon légitime de dire « saute à cette page, laisse le zoom tranquille » plutôt qu'un oubli. Le zoom lui-même est stocké comme une fraction de la valeur que vous transmettez, si bien qu'un appel demandant 150 pour cent donne au tableau une valeur stockée de 1.5, et la plage d'entrée valide est 0 à 6400

Comment lier à un PDF intégré à l'intérieur de votre propre document ?

AddLinkToEmbeddedPDF construit l'action GoToE, et son argument cible, EmbeddedFileName, est un nom plutôt qu'un chemin : il doit correspondre à la chaîne Title déjà transmise à EmbedFile lorsque la pièce jointe a été faite, car ce titre est la clé littérale que PDFlibPas stocke dans l'arbre de noms /EmbeddedFiles du document, et GoToE se résout en recherchant ce nom, pas en touchant à nouveau le système de fichiers. La fonction ne vérifie que EmbeddedFileName est non vide et que TargetPage vaut au moins 1 — transmettez un nom qui n'a jamais été réellement intégré et l'appel renvoie tout de même un succès, l'action est tout de même écrite, et le lien échoue simplement à se résoudre pour chaque lecteur qui clique dessus

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.NewPage;
    // The Title argument becomes the key PDFlibPas stores in the
    // document's EmbeddedFiles name tree -- that string, not
    // "datasheet.pdf", is the target GoToE resolves against.
    if Lib.EmbedFile('Datasheet', 'datasheet.pdf', 'application/pdf') = 1 then
      Lib.AddLinkToEmbeddedPDF(72, 700, 200, 16, 'Datasheet', 3, 0, 0);
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Deux planchers de version s'empilent ici, pas un seul. EmbedFile a besoin de PDF 1.4 pour l'arbre de noms /EmbeddedFiles, et AddLinkToEmbeddedPDF élève séparément le plancher à PDF 1.6 pour le type d'action GoToE lui-même, si bien que le minimum effectif pour tout document utilisant cette fonctionnalité est 1.6, pas 1.4. Remarquez aussi que TargetPage ici est basé sur 1, la convention PDFlibPas ordinaire — un contraste délibéré avec le DestPage basé sur zéro que la section précédente vient de couvrir, et un rappel que quel schéma de numérotation de page s'applique dépend du type d'action et de l'appel spécifique, pas d'une règle générale unique. Le dictionnaire cible de l'action peut aussi porter une entrée /R valant C pour enfant ou P pour parent, prenant en charge une chaîne à deux sauts vers un fichier intégré ou de retour vers son conteneur, bien qu'AddLinkToEmbeddedPDF ne construise jamais que la direction enfant, puisque c'est celle qui a du sens depuis un document effectuant l'intégration plutôt qu'étant intégré

Actions Launch : un FileName, deux cibles de chaîne qui ne sont pas interchangeables

SetActionLaunchOptions écrit la cible de fichier d'une action Launch vers deux clés différentes à partir d'un seul argument FileName, et les deux clés portent deux genres de chaîne différents. La clé de niveau supérieur /F reçoit un dictionnaire de spécification de fichier, construit via la même conversion de chemin que PDFlibPas utilise pour GoToR, qui est la forme portable qu'ISO 32000-1 §7.11.3 définit pour un dictionnaire de spécification de fichier. Le sous-dictionnaire /Win, quand PDFlibPas en écrit un, reçoit sa propre clé /F réglée sur la valeur FileName brute exactement telle que transmise, sans aucune conversion, car /Win /F est documenté dans ISO 32000-1 §12.6.4.5 comme une simple chaîne de chemin Windows destinée uniquement à être lue par une visionneuse Windows. Transmettez un chemin portable, déjà converti, en vous attendant à ce que les deux clés finissent identiques et la copie /Win portera quelle que soit la valeur que vous avez donnée à la fonction, non touchée

var
  Lib: TPDFlib;
  ActionID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('manual.pdf', '') = 1 then
    begin
      Lib.SelectPage(1);
      Lib.AddLinkToLocalFile(72, 660, 220, 16, 'calibrate.exe', 0);
      ActionID := Lib.GetAnnotActionID(1);
      // Operation 0 leaves this as a normal open -- pass 1 to ask a
      // Windows viewer to print instead. Parameters and
      // DefaultDirectory only ever reach /Win /P and /Win /D, never
      // the top-level /F.
      Lib.SetActionLaunchOptions(ActionID, 'calibrate.exe',
        '/silent /profile:default', 'C:\Tools\Calibration', 0, -1);
    end;
  finally
    Lib.Free;
  end;
end;

Traitez Launch comme l'action à plus haute friction des trois, car son but entier est d'exécuter un programme ou d'ouvrir un fichier en dehors du bac à sable PDF, et chaque visionneuse grand public la traite en conséquence. La Sécurité renforcée d'Adobe Acrobat bloque ou invite sur les actions Launch par défaut à moins que la cible ne se trouve dans un emplacement explicitement fiable, et la plupart des déploiements Acrobat d'entreprise laissent cette protection activée. Une action Launch dans un document remis au public n'est donc pas un déclencheur fiable : prévoyez qu'elle soit bloquée, qu'une invite apparaisse, ou qu'elle soit silencieusement ignorée par quelle que soit la visionneuse qui ouvre le fichier, et réservez-la aux environnements fermés où vous contrôlez aussi les réglages de confiance de la visionneuse — un kiosque interne, un déploiement d'entreprise contrôlé, un document qui ne quitte jamais une machine que vous gérez

Le portail PDF/A : pourquoi les appels GoToR et Launch peuvent renvoyer zéro

SetActionRemoteDestinationEx et SetActionLaunchOptions refusent tous deux purement et simplement quand le document cible est dans un mode de conformité PDF/A quelconque : les deux vérifient le mode PDF/A du document comme toute première condition et sortent avec un résultat de 0 avant de toucher à l'action, aucune exception levée. C'est délibéré. Les restrictions de PDF/A sur les actions interactives excluent spécifiquement Launch, puisque donner à un fichier d'archive la capacité d'exécuter un programme arbitraire est exactement le genre de comportement dépendant de l'environnement que les formats d'archivage à long terme existent pour empêcher, et PDFlibPas applique le même portail conservateur au définisseur de saut distant dans le même chemin de code. La conséquence pratique est facile à manquer pendant le développement : l'appel identique qui fonctionne sur un PDF ordinaire compilera, s'exécutera, et ne fera silencieusement rien sur un document chargé avec un niveau de conformité PDF/A défini, donc vérifiez la valeur de retour plutôt que de supposer un succès — un 0 ici n'est pas une erreur d'entrée malformée, c'est la bibliothèque refusant une demande qui entre en conflit avec la propre déclaration de conformité du document

Où GoToR, GoToE, et Launch s'intègrent-ils dans un flux de travail PDFlibPas plus large

Les trois types d'action de cet article n'atteignent pas tous les mêmes endroits. L'article compagnon sur les déclencheurs d'action de cycle de vie de document et de page couvre SetDocumentAction et SetPageAction, qui peuvent attacher une action GoToR ou Launch à un déclencheur comme WillClose via les constantes partagées PDF_ACTION_BUILDER_REMOTE_DESTINATION et PDF_ACTION_BUILDER_LAUNCH — le même constructeur qui couvre aussi un simple déclencheur URI ou JavaScript. GoToE n'a aucune constante de ce genre et aucun chemin du tout vers ce constructeur générique ; AddLinkToEmbeddedPDF est le seul moyen par lequel PDFlibPas en construit une, ce qui en fait strictement une action de zone de page, jamais un déclencheur au niveau document ou page. Là où GoToR et Launch atteignent bien le constructeur générique, le compromis est le contrôle : il construit un GoToR pointant seulement vers une destination distante nommée et une action Launch avec juste un nom de fichier et des paramètres, tandis que l'adressage explicite page-et-type-d'ajustement et les options de lancement spécifiques à Windows couvertes dans cet article ne sont atteints que via SetActionRemoteDestinationEx et SetActionLaunchOptions directement

Une propriété de sécurité mérite d'être connue avant de construire un outil de maintenance autour de ces définisseurs. SetActionRemoteDestinationEx et SetActionLaunchOptions construisent d'abord toute l'action de remplacement dans un dictionnaire de travail, et ne suppriment et copient les clés /F, /D ou /Win, et /NewWindow sur l'action vivante qu'une fois que cette copie de travail se valide — si bien qu'un appel qui échoue à la validation, que ce soit à cause d'un ValueMask hors plage ou d'un FileName vide, laisse l'action originale, et toute chaîne /Next déjà accrochée à elle, complètement intacte plutôt qu'à moitié écrasée. Cela compte car les actions GoToR et Launch peuvent toutes deux se trouver à l'intérieur d'une chaîne /Next construite avec AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, ou le plus général AddActionNextEx, permettant à un seul déclencheur de faire feu d'abord une entrée de journal JavaScript puis un saut distant en séquence. La construction de GoToR, GoToE, et Launch décrite ici fait partie de PDFlibPas, la bibliothèque PDF native pour Delphi et C++Builder