HotPDF añade marcadores a un PDF existente en Delphi a través de AddLoadedOutline, que crea entradas de esquema (outlines) en cualquier nivel de anidamiento sobre un documento cargado, y lee y escribe destinos con nombre mediante ResolveLoadedNamedDestination y AddLoadedNamedDestination. Cargue el archivo, construya el árbol, llame a SaveLoadedDocument y la barra lateral del lector que solía estar en blanco ahora contendrá una tabla de contenidos funcional
El escenario que motiva todo esto es desalentadoramente común. Se fusiona una docena de contratos en un único paquete de revisión, o se combinan tres manuales de producto en un único archivo distribuible, y el resultado es estructuralmente correcto: cada página está presente y cada fuente intacta. Pero luego alguien lo abre en Acrobat y el panel de marcadores está vacío. Un documento de 400 páginas sin navegación es técnicamente completo y prácticamente inutilizable, y hasta la versión 2.347.0 HotPDF podía leer, modificar y eliminar marcadores en documentos cargados, pero nunca crearlos. Esa carencia ya se ha resuelto, y este artículo detalla la nueva interfaz de la API y una particularidad del estándar PDF, la clave /Count, que le causará problemas si la trata como un simple contador de entradas
Qué es en realidad el árbol de esquemas (outlines)
Un esquema (outline) de PDF es un árbol de diccionarios doblemente enlazado, no una lista plana, y esa estructura explica cada parámetro de la API. La norma ISO 32000-1 §12.3.3 lo define: el catálogo del documento apunta a una raíz /Outlines, la raíz apunta a sus hijos primero y último mediante /First y /Last, y los hermanos se encadenan a través de /Next y /Prev mientras que cada entrada apunta hacia arriba a través de /Parent. Cada entrada contiene una cadena de texto /Title y, normalmente, un destino /Dest que indica a dónde debe llevar al lector al hacer clic en ella
Los destinos se presentan en dos variantes, y la norma ISO 32000-1 §12.3.2 las mantiene deliberadamente separadas. Un destino explícito es una matriz insertada como [page /XYZ x y zoom]: una referencia directa a un objeto de página más una especificación de vista. Por el contrario, un destino con nombre almacena una cadena de texto de nombre, y el objetivo real reside en el catálogo del documento bajo el árbol de nombres /Names /Dests, un nivel de indirección que permite que muchos enlaces compartan un único objetivo y que este se mueva sin alterar los enlaces. HotPDF escribe destinos /XYZ explícitos cuando crea marcadores, y le ofrece llamadas independientes para leer y ampliar el árbol de nombres
¿Cómo añadir marcadores a un PDF existente en Delphi?
Llame a AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) después de LoadFromFile: pase ParentIndex = -1 para una entrada de nivel superior, o el índice basado en cero de una entrada de nivel superior existente para anidarla debajo de ella. La función crea la raíz /Outlines si el documento nunca tuvo una, construye el diccionario de entrada, escribe una matriz /Dest [page /XYZ x y zoom] que apunta al índice DestPageIndex basado en cero y enlaza la nueva entrada en la cadena de hermanos. Devuelve 0 si tiene éxito y -1 en caso de fallo, por ejemplo, cuando DestPageIndex está fuera de rango
Un detalle de enlace es importante para el orden: una nueva entrada se inserta en la cabecera de la cadena de hijos de su padre, como la nueva /First. Por lo tanto, si añade "Capítulo 1" y luego "Capítulo 2" como entradas de nivel superior, el Capítulo 2 aparecerá por encima del Capítulo 1 en la barra lateral, y el Capítulo 1 se habrá desplazado al índice de nivel superior 1. El patrón práctico consiste en añadir las entradas en orden inverso de lectura, o añadir cada elemento padre y rellenar inmediatamente sus hijos mientras este sigue situado en el índice 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Añadir capítulos en orden inverso de lectura: cada nueva
// entrada de nivel superior pasa a ser /First (índice 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// El Capítulo 1 se encuentra ahora en el índice 0 de nivel superior; anidar secciones
// debajo de él (los hijos también se anteponen, por lo que el orden es inverso).
Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
end;
finally
Pdf.Free;
end;
end;
Los parámetros X, Y y Zoom se asocian directamente con el destino /XYZ y todos tienen el valor predeterminado 0. El estándar PDF sitúa el origen de coordenadas en la esquina inferior izquierda, por lo que Y = 792 apunta a la parte superior de una página tamaño Carta estadounidense y, según la norma ISO 32000-1, un valor cero en una posición /XYZ indica al visor que mantenga su configuración actual, que es exactamente lo que se desea para el zoom en casi todos los casos. Todo sucede sobre el grafo de objetos cargado en memoria; nada toca el disco hasta que se ejecuta SaveLoadedDocument, el mismo modelo de editar y luego guardar que utiliza la API de documentos cargados para reescribir el título, autor y metadatos XMP en PDFs cargados
Redireccionamiento de marcadores: saltos de página frente a acciones URI
Los marcadores existentes se pueden reconfigurar sin necesidad de volver a crearlos, y HotPDF ofrece dos llamadas diferentes para ambos casos debido a que las estructuras PDF subyacentes son realmente distintas. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) sustituye el /Dest de la entrada de nivel superior en Index por una nueva matriz /XYZ, que es la herramienta para resolver el clásico fallo posterior a la fusión en el que cada marcador sigue apuntando a los números de página del archivo original antes de la fusión. SetLoadedOutlineURI(Index, URI) realiza algo estructuralmente diferente: añade un diccionario de acción /A con /S /URI, convirtiendo el marcador en un enlace web en lugar de un salto interno dentro del documento
// La fusión desplazó todo 10 páginas: redirigir el
// tercer marcador de nivel superior a la página 12 (11 basado en cero).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Convertir la última entrada en un enlace externo.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Mantenga ambos mecanismos claros al diseñar el árbol. Un /Dest is navegación pura dentro del documento; una acción URI sale completamente del documento, y la norma ISO 32000-1 prevé que una entrada de esquema navegue a través de uno u otro mecanismo, pero no ambos. Por lo tanto, reserve SetLoadedOutlineURI para entradas que no tengan ya un destino de página, como un enlace de "Página de inicio del producto" o "Notificar un problema" al final del árbol en lugar de un encabezado de capítulo modificado. La acción URI escrita aquí es la misma estructura /S /URI que se utiliza para las anotaciones de enlace en el contenido de la página, cubierta con más detalle en la guía sobre la creación de hipervínculos en documentos PDF con HotPDF
¿Cómo funcionan los destinos con nombre en un PDF cargado?
Los destinos con nombre constituyen el mecanismo de anclaje estable de la norma ISO 32000-1 §12.3.2.3: en lugar de incrustar una referencia de página en cada enlace, el documento mantiene una asignación de nombre a destino en el árbol de nombres /Names /Dests del catálogo, y los enlaces hacen referencia a las entradas por su nombre. ResolveLoadedNamedDestination(Name) busca un nombre en ese árbol y devuelve el índice de página basado en cero al que apunta, o -1 si el nombre no está presente. El solucionador gestiona ambas formas de almacenamiento que la especificación permite para el valor mapeado: una matriz de destino simple como [page /XYZ x y zoom] y la forma de diccionario que envuelve la matriz bajo una clave /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) funciona a la inversa: registra un nuevo anclaje en el árbol /Names /Dests, creando el propio árbol cuando el documento no dispone de ninguno, y devuelve True si tiene éxito. En conjunto, ambas funciones cubren el flujo de trabajo en el que un documento fusionado debe respetar los anclajes que escribieron las herramientas anteriores y publicar otros nuevos para que los enlaces posteriores apunten a ellos
var
PageIdx: Integer;
begin
// Respetar un anclaje que otra herramienta escribió en /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publicar un nuevo anclaje para que otros enlaces lo tomen como objetivo.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Los límites honestos: los destinos que escribe HotPDF, tanto para marcadores como para anclajes con nombre, son destinos /XYZ. Los otros tipos explícitos que define la especificación, /Fit, /FitH, /FitB y similares, no se emiten en estas llamadas, aunque un destino /XYZ con zoom cero cubre el caso de uso principal de "ir a este lugar, mantener mi zoom". And GetLoadedBookmarkPageIndex(Title), la consulta de conveniencia que recibe el título de un marcador y devuelve su página de destino, analiza el nivel superior del árbol de esquemas, por lo que debe utilizarse para verificar las entradas a nivel de capítulo recién creadas en lugar de las hojas anidadas profundamente
La trampa de /Count: cuenta los descendientes, no las entradas
La clave /Count en la raíz del esquema no significa lo que la mayoría de los desarrolladores suponen, y una suposición errónea produce fallos en las pruebas que parecen una corrupción de datos. La norma ISO 32000-1 §12.3.3 define /Count como el número total de descendientes visibles en todos los niveles, no como el número de entradas de nivel superior. En una entrada individual, el signo también aporta significado: un valor positivo indica que la entrada está abierta y que ese número de descendientes se muestra en la barra lateral, mientras que un valor negativo indica que la entrada está colapsada y que |N| descendientes están ocultos dentro de ella
Esto surgió del propio conjunto de pruebas de regresión de HotPDF mientras se creaban las API de marcadores para documentos cargados. Un documento de prueba contenía tres marcadores de nivel superior, uno de ellos con un hijo; tras eliminar una entrada de nivel superior, GetLoadedOutlineCount (que lee la clave /Count de la raíz) informó 1 en lugar del 2 esperado. No había nada corrompido: el valor inicial 3 nunca había sido "tres entradas de nivel superior" en primer lugar, y el cálculo correcto debe recorrer la cadena de nivel superior acumulando cada nodo más su valor /Count positivo, omitiendo los descendientes de los nodos colapsados. La lección para su propio código es directa: nunca realice aserciones sobre diferencias en /Count tras ediciones estructurales, porque el valor cambia de forma no lineal con los descendientes visibles. Verifique la estructura por su contenido en su lugar
// Tras las ediciones, verificar mediante búsqueda de título, no por diferencias en /Count.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Esquema verificado');
Dónde encaja esto en el conjunto de herramientas de documentos cargados
La creación de marcadores y destinos con nombre completa un panorama que la API de documentos cargados ha estado cubriendo durante un tiempo: el mismo ciclo de cargar-editar-guardar ya reescribe los metadatos del documento, añade anotaciones de enlace y realiza el ciclo de ida y vuelta de anotaciones a través de la importación y exportación de XFDF. El patrón es uniforme: LoadFromFile, una secuencia de llamadas *Loaded* sobre el grafo de objetos en memoria y, finalmente, un SaveLoadedDocument, lo que facilita integrar la construcción de esquemas en una canalización de fusión o estampado existente como un paso adicional antes del guardado
Las API analizadas aquí (AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination y GetLoadedBookmarkPageIndex) se distribuyen con el Componente HotPDF estándar para Delphi y C++Builder, sin dependencias externas y sin requerir un visor ejecutable independiente