Artículo técnico

Acciones GoToR, GoToE y Launch en PDF de Delphi

PDFlibPas ofrece a los desarrolladores Delphi y C++Builder tres tipos de acción para navegación que abandona la página actual: GoToR (Go To Remote) abre una página concreta en otro archivo PDF, GoToE (Go To Embedded) abre un PDF embebido 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 parecen 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 precisamente 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 merece la pena distribuir dentro del manual en lugar de junto a él, un enlace que se entrega 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 ya escribió en un archivo algún otro productor; esta cubre construir esos mismos tres tipos de acción desde cero, incluidas las reglas a nivel de campo que hace cumplir PDFlibPas antes de confirmar un solo byte

Tres formas de que una acción PDF abandone 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 sitúa 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 Action Types que también define la acción GoTo cotidiana. El destino de una simple acción GoTo nombra un objeto de página que ya existe dentro del documento, así que PDFlibPas puede validarla de inmediato; GoToR y GoToE no pueden hacerlo del mismo modo, ya que el archivo externo puede que ni siquiera exista en esta máquina y el recuento de páginas de un archivo embebido no es algo que rastree el documento anfitrión, así que ambas 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 embebido más una página de destino para GoToE, mientras que Launch abandona por completo el concepto de destino y simplemente nombra algo que el sistema operativo debe ejecutar o abrir. Esa división aparece como dos familias de llamada en el lado de escritura: constructores de alto nivel, de una sola vez, como AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF y AddLinkToLocalFile crean juntos una anotación de enlace de zona activa de página y su acción, cubriendo la mayoría de las disposiciones reales, una línea de texto o un icono en el que hace clic un lector, mientras que establecedores de nivel más bajo como SetActionRemoteDestinationEx, SetActionLaunchOptions, y sus contrapartidas AddActionNext* adjuntan o sustituyen una acción sobre algo de lo que ya tenéis un handle: un marcador existente, un disparador de campo de formulario, o un evento de ciclo de vida a nivel de documento o de página. Ambas familias acaban escribiendo las mismas formas de diccionario; la diferencia está en dónde estáis parados cuando las llamáis, y, como cubre la siguiente sección, qué significa un número de página cuando lo hacéis

¿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 cualquier otro sitio, incluido SelectPage. SetActionRemoteDestinationEx, el establecedor de nivel más bajo usado para adjuntar o sustituir una acción GoToR sobre algo de lo que ya tenéis un handle, en cambio valida DestPage como mayor o igual que cero y lo escribe directamente en el array de destino explícito de la acción sin ningún ajuste: quiere el índice de página en bruto, con base cero, del documento de destino, la numeración que especifica ISO 32000-1 para un destino explícito remoto. Llamad al establecedor de bajo nivel con el mismo número que le entregaríais al constructor de alto nivel y el enlace se abre una página antes de tiempo

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 solo aceptan su única coordenada relevante. Los bits que dejéis sin activar dentro de una máscara por lo demás válida no se omiten del array; se escriben como un null 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 «saltad a esta página, dejad el zoom tal cual» en lugar de un descuido. El propio zoom se almacena como una fracción del valor que pasáis, así que una llamada que pide 150 por ciento entrega al array un valor almacenado de 1,5, y el rango de entrada válido es de 0 a 6400

¿Cómo se enlaza a un PDF embebido 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 se resuelve buscando ese nombre, no volviendo a tocar el sistema de archivos. La función solo comprueba que EmbeddedFileName no esté vacío y que TargetPage sea al menos 1, pasadle un nombre que nunca llegó a embeberse realmente y la llamada aun así devuelve éxito, la acción aun así se escribe, y el enlace simplemente no se resuelve para cada lector que haga 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 suelos de versión, no uno. EmbedFile necesita PDF 1.4 para el árbol de nombres /EmbeddedFiles, y AddLinkToEmbeddedPDF eleva por separado el suelo 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. Notad también que TargetPage aquí está basado en 1, la convención ordinaria de PDFlibPas, un contraste deliberado con el DestPage de base cero que acaba de cubrir la sección anterior, y un recordatorio de que qué esquema de numeración de página se aplica depende del tipo de acción y de la llamada concreta, no de una regla única y general. El diccionario de destino de la acción también puede llevar una entrada /R de C para hijo o P para padre, admitiendo una cadena de dos saltos hacia un archivo embebido o de vuelta hacia su contenedor, aunque AddLinkToEmbeddedPDF solo construye jamás la dirección hijo, ya que es la que tiene sentido desde un documento que hace el embebido en lugar de estar siendo embebido

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 portable 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 puesta al valor FileName en bruto 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 Windows pensada solo para que la lea un visor Windows. Pasadle una ruta portable, ya convertida, esperando que ambas claves acaben siendo idénticas y la copia /Win llevará cualquiera que sea lo que le entregasteis 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;

Tratad Launch como la acción de mayor fricción de las tres, porque todo su propósito es ejecutar un programa o abrir un archivo fuera del sandbox del PDF, y cada visor convencional la trata en consecuencia. La Enhanced Security de Adobe Acrobat bloquea o pregunta ante las acciones Launch por defecto a menos que el destino resida en una ubicación explícitamente de confianza, 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 tanto, no es un disparador fiable: planificad que quede bloqueada, que se pregunte por ella, o que la ignore en silencio cualquiera que sea el visor que abra el archivo, y reservadla para entornos cerrados donde también controléis los ajustes de confianza del visor, un quiosco interno, un despliegue corporativo controlado, un documento que nunca sale de una máquina que gestionáis vosotros

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

SetActionRemoteDestinationEx y SetActionLaunchOptions se niegan de plano cuando el documento de destino está en cualquier modo de conformidad PDF/A: ambas 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 dar 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 compuerta conservadora al establecedor de ir-a remoto en la misma vía 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 en silencio no hará nada en un documento cargado con un nivel de conformidad PDF/A establecido, así que comprobad el valor de retorno en lugar de asumir éxito, un 0 aquí no es un error de entrada mal formada, 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 PDFlibPas más amplio

Los tres tipos de acción de este artículo no llegan todos a los mismos sitios. El artículo complementario sobre los 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 a través de las constantes compartidas PDF_ACTION_BUILDER_REMOTE_DESTINATION y PDF_ACTION_BUILDER_LAUNCH, el mismo constructor que también cubre un simple disparador URI o JavaScript. GoToE no tiene ninguna constante así ni ninguna vía hacia ese constructor genérico en absoluto; AddLinkToEmbeddedPDF es la única forma en que PDFlibPas construye una, lo que la convierte estrictamente en una acción de zona activa de página, nunca en un disparador a nivel de documento o de 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 por 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

Merece la pena conocer una propiedad de seguridad antes de construir una herramienta de mantenimiento en torno a estos establecedores. SetActionRemoteDestinationEx y SetActionLaunchOptions construyen primero toda la acción de sustitución en un diccionario provisional, y solo eliminan y copian las claves /F, /D o /Win, y /NewWindow a la acción en vivo en cuanto esa copia provisional se valida, así que una llamada que falla la validación, ya sea por un ValueMask fuera de rango o por un FileName vacío, deja la acción original, y cualquier cadena /Next ya colgando de ella, completamente intacta en lugar de a medio sobrescribir. Eso importa porque las acciones GoToR y Launch pueden residir ambas dentro de una cadena /Next construida con AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, o el más general AddActionNextEx, permitiendo que un único disparador dispare una entrada de registro JavaScript y después un salto remoto en secuencia. La construcción de GoToR, GoToE y Launch descrita aquí forma parte de PDFlibPas, la biblioteca PDF nativa para Delphi y C++Builder