Artículo técnico

Acciones GoToR, GoToE, y Launch en PDF de Delphi

PDFlibPas le da a los desarrolladores Delphi y C++Builder tres tipos de acción para navegación que deja atrás la página actual: GoToR (Go To Remote) abre una página específica en otro archivo PDF, GoToE (Go To Embedded) abre un archivo PDF incrustado dentro del documento actual, y Launch ejecuta un programa externo o abre un archivo a través del shell del sistema operativo. Los tres viven en ISO 32000-1 §12.6.4, la sección de Tipos de Acción que también define la acción GoTo cotidiana, y cada uno lleva su propia trampa para el descuidado: un número de página que significa algo distinto según qué llamada lo construya, un destino que es un nombre en lugar de una ruta de archivo, y un par de parámetros de cadena que se ven idénticos pero sirven a dos visores distintos

Nada de esto es hipotético. Un paquete de referencia técnica —un manual principal, un PDF de especificaciones que un distribuidor actualiza según su propio calendario, una utilidad de calibración instalada junto a ambos— se apoya exactamente en este tipo de cableado entre documentos: una referencia cruzada que tiene que aterrizar en la página 5 del archivo de especificaciones, una hoja de datos que vale la pena enviar dentro del manual en lugar de al lado de él, un enlace que se pasa directamente a la herramienta de calibración. Este artículo es la imagen especular de leer de vuelta acciones de marcador y anotación de un PDF existente: esa pieza cubre consumir una acción GoToR, Launch, o GoToE que algún otro productor ya escribió en un archivo; este cubre construir esos mismos tres tipos de acción desde cero, incluidas las reglas a nivel de campo que hace cumplir PDFlibPas antes de comprometer un solo byte

Tres maneras en que una acción de PDF puede dejar atrás la página actual

PDFlibPas separa la navegación local de todo lo demás en la clave /S de la acción, y GoToR, GoToE, y Launch son los tres subtipos cuyo destino se ubica fuera de la página actual: GoToR bajo ISO 32000-1 §12.6.4.3, GoToE bajo §12.6.4.4, y Launch bajo §12.6.4.5, todos dentro de la sección más amplia §12.6.4 Tipos de Acción que también define la acción GoTo cotidiana. El destino de una acción GoTo simple nombra un objeto de página que ya existe dentro del documento, así que PDFlibPas puede validarlo de inmediato; GoToR y GoToE no pueden hacer eso de la misma manera, ya que el archivo externo puede que ni siquiera exista en esta máquina y el conteo de páginas de un archivo incrustado no es algo que rastree el documento anfitrión, así que ambos llevan una referencia sin resolver en lugar de un enlace duro —una especificación de archivo más un destino para GoToR, un nombre de archivo incrustado más una página de destino para GoToE— mientras que Launch descarta el concepto de destino por completo y simplemente nombra algo para que el sistema operativo lo ejecute o abra. Esa división aparece como dos familias de llamada del lado de escritura: constructores de alto nivel, de una sola vez, como AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF, y AddLinkToLocalFile crean una anotación de enlace de zona activa de página y su acción juntas, cubriendo la mayoría de los diseños reales —una línea de texto o un icono en el que hace clic un lector— mientras que setters de nivel más bajo como SetActionRemoteDestinationEx, SetActionLaunchOptions, y sus contrapartes AddActionNext* adjuntan o reemplazan una acción en algo cuyo handle ya se posee: un marcador existente, un disparador de campo de formulario, o un evento de ciclo de vida a nivel de documento o página. Ambas familias terminan escribiendo las mismas formas de diccionario; la diferencia está en dónde se está parado cuando se las llama, y, como cubre la siguiente sección, qué significa un número de página cuando se hace

¿Cómo se construye un enlace GoToR que abre una página en otro archivo PDF?

Una acción GoToR necesita dos cosas —una especificación de archivo y un destino dentro de ese archivo— y PDFlibPas expone dos llamadas distintas para suministrar la segunda parte, cada una con su propia convención de numeración de página. AddLinkToFile y AddLinkToFileEx, los constructores de alto nivel de zona activa de página, validan su argumento Page o DestPage como mayor que cero, la misma numeración basada en 1 que usa PDFlibPas en todas partes, incluido SelectPage. SetActionRemoteDestinationEx, el setter de nivel más bajo usado para adjuntar o reemplazar una acción GoToR en algo de lo que ya se tiene un handle, en cambio valida DestPage como mayor o igual que cero y lo escribe directamente en el arreglo de destino explícito de la acción sin ningún ajuste: quiere el índice de página crudo, basado en cero, del documento de destino, la numeración que ISO 32000-1 especifica para un destino explícito remoto. Llame al setter de bajo nivel con el mismo número que le entregaría al constructor de alto nivel y el enlace abre una página antes

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;

El resto de los argumentos de SetActionRemoteDestinationEx son igual de literales. ValueMask es un conjunto de bits —1 para izquierda, 2 para arriba, 4 para derecha, 8 para abajo, 16 para zoom— y PDFlibPas lo comprueba contra DestType antes de escribir nada: un destino dkFitR debe suministrar exactamente 15 (los cuatro bordes, sin zoom), dkFit y dkFitB deben suministrar 0, y dkFitH/dkFitV aceptan solo su única coordenada relevante. Los bits que se dejan sin establecer dentro de una máscara por lo demás válida no se omiten del arreglo; se escriben como un nulo PDF explícito, que ISO 32000-1 trata como "conservar cualquiera que sea el valor que ya tenga el visor" para esa coordenada —una forma legítima de decir "salta a esta página, deja el zoom en paz" en lugar de un descuido. El zoom mismo se almacena como una fracción del valor que se pasa, así que una llamada que pide 150 por ciento le entrega al arreglo un valor almacenado de 1.5, y el rango de entrada válido es 0 a 6400

¿Cómo se enlaza a un PDF que está incrustado dentro del propio documento?

AddLinkToEmbeddedPDF construye la acción GoToE, y su argumento de destino, EmbeddedFileName, es un nombre en lugar de una ruta: tiene que coincidir con la cadena Title ya pasada a EmbedFile cuando se hizo el adjunto, porque ese título es la clave literal que almacena PDFlibPas en el árbol de nombres /EmbeddedFiles del documento, y GoToE resuelve buscando ese nombre, no tocando el sistema de archivos de nuevo. La función solo comprueba que EmbeddedFileName no esté vacío y que TargetPage sea al menos 1 —entréguele un nombre que nunca se incrustó realmente y la llamada igual devuelve éxito, la acción igual se escribe, y el enlace simplemente falla al resolverse para cada lector que hace clic en él

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;

Aquí se apilan dos pisos de versión, no uno. EmbedFile necesita PDF 1.4 para el árbol de nombres /EmbeddedFiles, y AddLinkToEmbeddedPDF por separado eleva el piso a PDF 1.6 para el propio tipo de acción GoToE, así que el mínimo efectivo para cualquier documento que use esta característica es 1.6, no 1.4. Note también que TargetPage aquí está basado en 1, la convención ordinaria de PDFlibPas —un contraste deliberado con el DestPage basado en cero que acaba de cubrir la sección anterior, y un recordatorio de que qué esquema de numeración de página aplica depende del tipo de acción y la llamada específica, no de una regla general. El diccionario de destino de la acción también puede llevar una entrada /R de C para hijo o P para padre, soportando una cadena de dos saltos hacia un archivo incrustado o de vuelta hacia su contenedor, aunque AddLinkToEmbeddedPDF solo construye la dirección hijo, ya que es la que tiene sentido desde un documento que hace la incrustación en lugar de ser incrustado

Acciones Launch: un FileName, dos destinos de cadena que no son intercambiables

SetActionLaunchOptions escribe el destino de archivo de una acción Launch en dos claves distintas a partir de un único argumento FileName, y las dos claves contienen dos tipos de cadena distintos. La clave de nivel superior /F recibe un diccionario de especificación de archivo, construido a través de la misma conversión de ruta que usa PDFlibPas para GoToR, que es la forma portátil que define ISO 32000-1 §7.11.3 para un diccionario de especificación de archivo. El subdiccionario /Win, cuando PDFlibPas escribe uno, recibe su propia clave /F establecida al valor crudo de FileName exactamente tal como se pasó, sin ninguna conversión en absoluto, porque /Win /F está documentado en ISO 32000-1 §12.6.4.5 como una simple cadena de ruta de Windows destinada solo a que la lea un visor de Windows. Pase una ruta portátil, ya convertida, esperando que ambas claves terminen idénticas y la copia /Win llevará cualquiera que sea lo que se le entregó a la función, sin tocar

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;

Trate Launch como la acción de mayor fricción de las tres, porque su propósito completo es ejecutar un programa o abrir un archivo fuera del sandbox de PDF, y cada visor convencional la trata en consecuencia. La Seguridad Mejorada de Adobe Acrobat bloquea o pregunta ante acciones Launch por defecto a menos que el destino esté en una ubicación explícitamente confiable, y la mayoría de los despliegues empresariales de Acrobat dejan esa protección activada. Una acción Launch en un documento entregado al público por lo tanto no es un disparador confiable: planifique que sea bloqueada, preguntada, o silenciosamente ignorada por cualquiera que sea el visor que abra el archivo, y resérvela para entornos cerrados donde también controle la configuración de confianza del visor —un kiosco interno, un despliegue corporativo controlado, un documento que nunca sale de una máquina que usted administra

La puerta PDF/A: por qué las llamadas GoToR y Launch pueden devolver cero

SetActionRemoteDestinationEx y SetActionLaunchOptions ambos se niegan directamente cuando el documento de destino está en cualquier modo de conformidad PDF/A: ambos comprueban el modo PDF/A del documento como su primerísima condición y salen con un resultado de 0 antes de tocar la acción, sin lanzar ninguna excepción. Esto es deliberado. Las restricciones de PDF/A sobre acciones interactivas descartan Launch específicamente, ya que darle a un archivo de archivado la capacidad de ejecutar un programa arbitrario es exactamente el tipo de comportamiento dependiente del entorno que los formatos de archivado a largo plazo existen para prevenir, y PDFlibPas aplica la misma puerta conservadora al setter de ir-a remoto en la misma ruta de código. La consecuencia práctica es fácil de pasar por alto durante el desarrollo: la llamada idéntica que funciona en un PDF ordinario compilará, se ejecutará, y silenciosamente no hará nada en un documento cargado con un nivel de conformidad PDF/A establecido, así que compruebe el valor de retorno en lugar de asumir éxito —un 0 aquí no es un error de entrada malformada, es la biblioteca negándose a una solicitud que entra en conflicto con la propia declaración de conformidad del documento

Dónde encajan GoToR, GoToE, y Launch en un flujo de trabajo de PDFlibPas más grande

Los tres tipos de acción de este artículo no llegan todos a los mismos lugares. El artículo complementario sobre disparadores de acción de ciclo de vida de documento y página cubre SetDocumentAction y SetPageAction, que pueden adjuntar una acción GoToR o Launch a un disparador como WillClose mediante las constantes compartidas PDF_ACTION_BUILDER_REMOTE_DESTINATION y PDF_ACTION_BUILDER_LAUNCH —el mismo constructor que también cubre un disparador URI o JavaScript simple. GoToE no tiene tal constante ni ninguna ruta hacia ese constructor genérico en absoluto; AddLinkToEmbeddedPDF es la única manera en que PDFlibPas construye una, lo que la hace estrictamente una acción de zona activa de página, nunca un disparador a nivel de documento o página. Donde GoToR y Launch sí llegan al constructor genérico, la compensación es control: construye un GoToR que apunta solo a un destino remoto con nombre y una acción Launch con solo un nombre de archivo y parámetros, mientras que el direccionamiento explícito de página y tipo de ajuste y las opciones de lanzamiento específicas de Windows cubiertas en este artículo solo se alcanzan directamente a través de SetActionRemoteDestinationEx y SetActionLaunchOptions

Una propiedad de seguridad vale la pena conocerla antes de construir una herramienta de mantenimiento alrededor de estos setters. SetActionRemoteDestinationEx y SetActionLaunchOptions construyen primero toda la acción de reemplazo en un diccionario de trabajo, y solo eliminan y copian las claves /F, /D o /Win, y /NewWindow a la acción en vivo una vez que esa copia de trabajo valida —así que una llamada que falla la validación, ya sea por un ValueMask fuera de rango o un FileName vacío, deja la acción original, y cualquier cadena /Next ya colgando de ella, completamente intacta en lugar de medio sobrescrita. Eso importa porque las acciones GoToR y Launch ambas pueden estar dentro de una cadena /Next construida con AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, o el más general AddActionNextEx, permitiendo que un solo disparador dispare una entrada de log de JavaScript y luego un salto remoto en secuencia. La construcción de GoToR, GoToE, y Launch como se describe aquí es parte de PDFlibPas, la biblioteca PDF nativa para Delphi y C++Builder