HotPDF agrega marcadores a un PDF existente en Delphi a través de AddLoadedOutline, que crea entradas de esquema en cualquier nivel de anidamiento sobre un documento cargado, y lee y escribe destinos con nombre a través de ResolveLoadedNamedDestination y AddLoadedNamedDestination. Cargue el archivo, construya el árbol, llame a SaveLoadedDocument, y la barra lateral del lector que antes estaba vacía ahora tendrá una tabla de contenidos funcional
El escenario que motiva todo esto es lamentablemente común. Usted fusiona una docena de contratos en un solo paquete de revisión, o impone tres manuales de producto en un único archivo distribuible, y el resultado es correcto desde el punto de vista estructural: cada página presente, cada fuente intacta. Luego, alguien lo abre en Acrobat y el panel de marcadores está vacío. Un documento de 400 páginas sin navegación está técnicamente completo pero es 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 brecha ya se ha cerrado, y este artículo analiza la nueva superficie de la API y el concepto particular de PDF, la clave /Count, que le causará problemas si la trata como un simple contador de entradas
Qué es realmente el árbol de esquema (outline tree)
Un esquema 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 primeros y últimos hijos a través de /First y /Last, y los hermanos se encadenan mediante /Next y /Prev, mientras que cada entrada apunta hacia arriba a través de /Parent. Cada entrada contiene una cadena /Title y, por lo general, 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 los mantiene separados de manera deliberada. Un destino explícito es una matriz integrada como [página /XYZ x y zoom]: una referencia directa a un objeto de página más una especificación de vista. En cambio, un destino con nombre almacena una cadena de nombre, y el destino real reside en el catálogo de documentos bajo el árbol de nombres /Names /Dests, lo que representa un nivel de indirección que permite que muchos enlaces compartan un solo destino y que el destino se mueva sin necesidad de modificar los enlaces. HotPDF escribe destinos /XYZ explícitos al crear marcadores y le ofrece llamadas independientes para leer y extender el árbol de nombres
¿Cómo agregar 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 entradas, escribe una matriz /Dest [página /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: se inserta una nueva entrada en la cabecera de la cadena de hijos de su elemento padre, como el nuevo /First. De este modo, si agrega el "Capítulo 1" y luego el "Capítulo 2" como entradas de nivel superior, el Capítulo 2 aparecerá arriba 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 agregar las entradas en orden inverso al de lectura, o agregar cada elemento padre y rellenar sus hijos de inmediato mientras este aún se encuentra en el índice 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Add chapters in reverse reading order: each new
// top-level entry becomes /First (index 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// Chapter 1 is now top-level index 0; nest sections
// under it (children also prepend, so reverse order).
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 asignan directamente al destino /XYZ y todos tienen el valor predeterminado de 0. 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 (US Letter), y según la norma ISO 32000-1, un valor de cero en una ranura /XYZ le indica al visor que mantenga su configuración actual, que es exactamente lo que desea para el zoom en casi todos los casos. Todo ocurre sobre el gráfico de objetos cargado en la memoria; nada toca el disco hasta que se ejecuta SaveLoadedDocument, el mismo modelo de edición y guardado que utiliza la API de documentos cargados para sobrescribir el título, el autor y los metadatos XMP en archivos PDF cargados
Redireccionamiento de marcadores: saltos de página frente a acciones URI
Los marcadores existentes se pueden volver a enlazar sin necesidad de crearlos de nuevo, y HotPDF ofrece dos llamadas diferentes para ambos casos porque las construcciones PDF subyacentes son verdaderamente distintas. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) reemplaza el /Dest de la entrada de nivel superior en el índice Index con una nueva matriz /XYZ, que es la herramienta ideal para 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: adjunta un diccionario de acción /A con /S /URI, convirtiendo el marcador en un enlace web en lugar de un salto dentro del documento
// The merge shifted everything by 10 pages: repoint the
// third top-level bookmark at page 12 (zero-based 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Turn the last entry into an external link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Mantenga claros ambos mecanismos cuando diseñe el árbol. Un /Dest es pura navegación dentro del documento; una acción URI sale por completo del documento, y la norma ISO 32000-1 espera que una entrada de esquema navegue a través de uno de los dos mecanismos, pero no ambos. Por lo tanto, reserve SetLoadedOutlineURI para las entradas que no tengan ya un destino de página, como un elemento hoja del tipo "Página de inicio del producto" o "Informar de un problema" al final del árbol, en lugar de un encabezado de capítulo convertido. La acción URI escrita aquí es la misma construcción /S /URI que se utiliza para las anotaciones de enlace en el contenido de las páginas, analizadas con mayor detalle en la guía para crear hipervínculos en documentos PDF con HotPDF
¿Cómo funcionan los destinos con nombre en un PDF cargado?
Los destinos con nombre representan 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 un mapa 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 existe. El resolvedor maneja ambas formas de almacenamiento que la especificación permite para el valor mapeado: una matriz de destino directa como [página /XYZ x y zoom] y la forma de diccionario que envuelve la matriz bajo una clave /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) realiza el proceso inverso: registra un nuevo anclaje en el árbol /Names /Dests, creando el propio árbol cuando el documento no tiene ninguno, y devuelve True si tiene éxito. En conjunto, el par cubre el flujo de trabajo en el que un documento fusionado debe respetar los anclajes escritos por herramientas anteriores y publicar otros nuevos para que sirvan de destino a los enlaces posteriores
var
PageIdx: Integer;
begin
// Honor an anchor another tool wrote into /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publish a new anchor for other links to target.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Los límites sinceros: 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, como /Fit, /FitH, /FitB y similares, no se emiten a través de estas llamadas, aunque /XYZ con zoom cero cubre el caso de uso predominante de "ir a este lugar, mantener mi zoom". And GetLoadedBookmarkPageIndex(Title), la búsqueda de conveniencia que recibe el título de un marcador y devuelve su página de destino, escanea el nivel superior del árbol de esquemas, por lo que debe utilizarla para verificar las entradas a nivel de capítulo que acaba de crear en lugar de hojas profundamente anidadas
La trampa de /Count: cuenta descendientes, no 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 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 el número de entradas de nivel superior. En una entrada individual, el signo también tiene significado: un valor positivo indica que la entrada está abierta y esa cantidad de descendientes se muestra en la barra lateral, mientras que un valor negativo indica que la entrada está colapsada y los descendientes |N| están ocultos dentro de ella
Esto surgió del propio conjunto de pruebas de regresión de HotPDF mientras se construían las API de marcadores de documentos cargados. Un documento de prueba contenía tres marcadores de nivel superior, uno de ellos con un hijo; después de eliminar una entrada de nivel superior, GetLoadedOutlineCount, que lee la propiedad de la raíz /Count, informó 1 en lugar del 2 esperado. Nada se había corrompido: el valor inicial de 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 colados. La lección para su propio código es directa: nunca realice aserciones sobre diferencias de /Count después de realizar modificaciones estructurales, debido a que el valor cambia de forma no lineal con los descendientes visibles. En su lugar, verifique la estructura por el contenido
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Dónde encaja esto dentro del conjunto de herramientas para documentos cargados
La creación de marcadores y destinos con nombre completa una funcionalidad que la API de documentos cargados ha estado desarrollando desde hace tiempo: el mismo ciclo de cargar-editar-guardar ya reescribe los metadatos del documento, agrega anotaciones de enlace y realiza el viaje de ida y vuelta de anotaciones a través de la importación y exportación XFDF. El patrón es uniforme: LoadFromFile, una secuencia de llamadas del tipo *Loaded* sobre el gráfico de objetos en memoria y, luego, un método SaveLoadedDocument, lo cual facilita la integración de la construcción del esquema en un flujo de fusión o sellado existente como un paso adicional antes de guardar
Las API analizadas aquí, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination, y GetLoadedBookmarkPageIndex, se incluyen en la versión estándar de HotPDF Component para Delphi y C++Builder, sin dependencias externas y sin requerir un visor independiente en tiempo de ejecución