HotPDF aggiunge segnalibri a un PDF esistente in Delphi tramite AddLoadedOutline, che crea voci di struttura a qualsiasi livello di nidificazione su un documento caricato, e legge e scrive destinazioni denominate tramite ResolveLoadedNamedDestination e AddLoadedNamedDestination. Carica il file, crea l'albero, chiama SaveLoadedDocument e la barra laterale del lettore, che prima era vuota, conterrà ora un sommario funzionante
Lo scenario alla base di tutto questo è tristemente comune. Si uniscono una dozzina di contratti in un unico pacchetto di revisione, o si collegano tre manuali di prodotto in un unico file distribuibile, e l'output è strutturalmente corretto: ogni pagina è presente, ogni font è intatto. Poi qualcuno lo apre in Acrobat e il pannello dei segnalibri risulta vuoto. Un documento di 400 pagine senza navigazione è tecnicamente completo ma praticamente inutilizzabile, e fino alla versione 2.347.0 HotPDF poteva leggere, modificare ed eliminare segnalibri sui documenti caricati, ma mai crearli. Questo divario è ora colmato, e questo articolo esamina la nuova API e l'unico elemento misterioso del formato PDF, la chiave /Count, che può causare problemi se trattata come un semplice contatore di voci
Cos'è effettivamente l'albero della struttura (outline tree)
Una struttura PDF (outline) è un albero di dizionari con collegamento doppio, non un elenco piatto, e questa struttura spiega ogni parametro nell'API. Lo standard ISO 32000-1 §12.3.3 la definisce così: il catalogo del documento punta a una radice /Outlines, la radice punta ai suoi primi e ultimi figli tramite /First e /Last, e gli elementi dello stesso livello si collegano tramite /Next e /Prev, mentre ogni voce punta a ritroso tramite /Parent. Ciascuna voce contiene una stringa /Title e, solitamente, un campo /Dest che indica dove cliccando l'utente deve essere reindirizzato
Le destinazioni sono di due tipi, e lo standard ISO 32000-1 §12.3.2 le mantiene volutamente separate. Una destinazione esplicita è un array in linea come [page /XYZ x y zoom]: un riferimento diretto a un oggetto pagina più una specifica di visualizzazione. Una destinazione denominata (named destination) memorizza invece una stringa di nome, e il target effettivo risiede nel catalogo del documento sotto l'albero dei nomi /Names /Dests, un livello di indirezione che consente a molti collegamenti di condividere un unico target e a quest'ultimo di spostarsi senza dover modificare i collegamenti. HotPDF scrive destinazioni esplicite /XYZ quando crea i segnalibri, e fornisce chiamate separate per leggere ed estendere l'albero dei nomi
Come si aggiungono segnalibri a un PDF esistente in Delphi?
Chiama AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) dopo LoadFromFile: passa ParentIndex = -1 per una voce di livello principale, oppure l'indice a base zero di una voce principale esistente per nidificarla al di sotto. La funzione crea la radice /Outlines se il documento non ne ha mai avuta una, compila il dizionario della voce, scrive un array /Dest [page /XYZ x y zoom] che punta all'indice di pagina DestPageIndex a base zero e collega la nuova voce nella catena di elementi adiacenti. Restituisce 0 in caso di successo e -1 in caso di errore, ad esempio se DestPageIndex è fuori intervallo
Un dettaglio di collegamento è importante per l'ordinamento: una nuova voce viene inserita all'inizio della catena di figli del relativo genitore, come nuovo elemento /First. Quindi, se si aggiungono "Capitolo 1" e poi "Capitolo 2" como voci di livello principale, il Capitolo 2 appare sopra il Capitolo 1 nella barra laterale, e il Capitolo 1 si sposta all'indice di livello principale 1. L'approccio pratico consiste nell'aggiungere le voci in ordine di lettura inverso, oppure nell'aggiungere ciascun genitore e popolare immediatamente i relativi elementi figli mentre si trova ancora all'indice 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;
I parametri X, Y e Zoom si mappano direttamente sulla destinazione /XYZ e hanno tutti come valore predefinito 0. Il formato PDF colloca l'origine delle coordinate nell'angolo in basso a sinistra, quindi Y = 792 punta alla parte superiore di una pagina US Letter, e secondo lo standard ISO 32000-1 un valore pari a zero in uno slot /XYZ indica al visualizzatore di mantenere le impostazioni correnti, che è esattamente ciò che si desidera per lo zoom in quasi tutti i casi. Tutto avviene sul grafo dell'oggetto caricato in memoria; nulla tocca il disco finché non viene eseguito SaveLoadedDocument, lo stesso modello edit-then-save utilizzato dall'API dei documenti caricati per riscrivere titolo, autore e metadati XMP sui PDF caricati
Reindirizzamento dei segnalibri: salti di pagina rispetto ad azioni URI
I segnalibri esistenti possono essere ricollegati senza doverli ricreare, e HotPDF fornisce due chiamate distinte per i due casi poiché i costrutti PDF sottostanti sono effettivamente diversi. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) sostituisce il valore /Dest della voce principale all'indice Index con un nuovo array /XYZ, utile per risolvere il classico problema post-unione in cui ogni segnalibri punta ancora ai numeri di pagina del file originale precedente all'unione. SetLoadedOutlineURI(Index, URI) esegue un'operazione strutturalmente diversa: associa un dizionario di azioni /A con /S /URI, trasformando il segnalibri in un collegamento web anziché in un salto all'interno 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');
Tenere distinti i due meccanismi quando si progetta l'albero. Un /Dest serve unicamente alla navigazione interna del documento; un'azione URI esce completamente dal documento, e lo standard ISO 32000-1 prevede che una voce di struttura navighi attraverso l'uno o l'altro meccanismo, non entrambi. Riserva quindi SetLoadedOutlineURI per le voci che non hanno già una destinazione di pagina, ad esempio un elemento foglia come "Pagina iniziale del prodotto" o "Segnala un problema" alla fine dell'albero, piuttosto che per intestazioni di capitolo convertite. L'azione URI scritta qui utilizza lo stesso costrutto /S /URI impiegato per le annotazioni di collegamento sul contenuto della pagina, trattato in maggior dettaglio nella guida alla creazione di collegamenti ipertestuali nei documenti PDF con HotPDF
Come funzionano le destinazioni denominate su un PDF caricato?
Le destinazioni denominate sono il meccanismo di ancoraggio stabile dello standard ISO 32000-1 §12.3.2.3: invece di incorporare un riferimento di pagina in ogni collegamento, il documento mantiene una mappa nome-destinazione nell'albero dei nomi /Names /Dests del catalogo, e i collegamenti fanno riferimento alle voci tramite il nome. ResolveLoadedNamedDestination(Name) cerca un nome in quell'albero e restituisce l'indice di pagina a base zero a cui punta, o -1 se il nome è assente. Il risolutore gestisce entrambe le forme di archiviazione consentite dalle specifiche per il valore mappato: un array di destinazione semplice come [page /XYZ x y zoom] e la forma dizionario che racchiude l'array sotto una chiave /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) esegue l'operazione inversa: registra una nuova ancora nell'albero /Names /Dests, creando l'albero stesso qualora il documento non ne contenga uno, e restituisce True in caso di successo. Insieme, la coppia di funzioni copre il flusso di lavoro in cui un documento unito deve rispettare le ancore scritte dagli strumenti a monte e pubblicarne di nuove come target per i collegamenti successivi
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;
I limiti oggettivi: le destinazioni scritte da HotPDF, sia per i segnalibri che per le ancore denominate, sono destinazioni /XYZ. Gli altri tipi espliciti definiti dalle specifiche, come /Fit, /FitH, /FitB e simili, non vengono emessi da queste chiamate, sebbene /XYZ con zoom a zero copra la stragrande maggioranza dei casi d'uso ("vai in questo punto, mantieni lo zoom corrente"). Inoltre, GetLoadedBookmarkPageIndex(Title), la ricerca rapida che accetta il titolo di un segnalibri e restituisce la pagina di destinazione, esegue la scansione solo del livello principale della struttura, quindi utilizzala per verificare le voci a livello di capitolo appena create piuttosto che le foglie nidificate in profondità
La trappola di /Count: conta i discendenti, non le voci
La chiave /Count sulla radice della struttura non corrisponde a ciò che la maggior parte degli sviluppatori presuppone, e un'ipotesi errata produce fallimenti nei test che sembrano indicare una corruzione dei dati. Lo standard ISO 32000-1 §12.3.3 definisce /Count come il numero totale di discendenti visibili a tutti i livelli, non il numero di voci di livello principale. Su una singola voce anche il segno ha un significato: un valore positivo indica che la voce è aperta e che quel numero di discendenti viene mostrato nella barra laterale, mentre un valore negativo indica che la voce è compressa e |N| discendenti sono nascosti al suo interno
Questo aspetto è emerso dalla suite di regressione di HotPDF durante lo sviluppo delle API dei segnalibri per i documenti caricati. Un documento di test conteneva tre segnalibri di livello principale, uno dei quali con un figlio; dopo aver eliminato una voce di livello principale, GetLoadedOutlineCount, che legge il valore /Count alla radice, ha restituito 1 anziché il valore atteso 2. Nulla era corrotto: il valore iniziale di 3 non indicava "tre voci di livello principale" in origine, e il ricalcolo corretto deve percorrere la catena principale accumulando ciascun nodo più il suo valore /Count positivo, saltando i discendenti dei nodi compressi. La lezione per il tuo codice è diretta: non fare mai affidamento sulle differenze di /Count dopo modifiche strutturali, poiché il valore varia in modo non lineare in base ai discendenti visibili. Verifica invece la struttura in base al contenuto
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Qual è la collocazione di questa funzionalità nel set di strumenti per documenti caricati
La creazione di segnalibri e destinazioni denominate completa un quadro che l'API per i documenti caricati delinea già da tempo: lo stesso ciclo load-edit-save riscrive già i metadati del documento, aggiunge annotazioni di collegamento e gestisce le annotazioni tramite l'importazione ed esportazione XFDF. Il pattern è uniforme, LoadFromFile, una sequenza di chiamate *Loaded* sul grafo dell'oggetto in memoria, quindi una chiamata a SaveLoadedDocument, il che rende immediato integrare la creazione della struttura in un flusso di unione o timbratura esistente come ulteriore passaggio prima del salvataggio
Le API qui trattate, ovvero AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination e GetLoadedBookmarkPageIndex, sono fornite nel Componente HotPDF standard per Delphi e C++Builder, senza dipendenze esterne e senza richiedere un runtime di visualizzazione separato