PDF Library for Delphi scrive i range dei page label con AddPageLabels, e dalla v3.539.10 quella chiamata funziona anche sui file caricati il cui number tree /PageLabels è spezzato in nodi /Kids: il root viene appiattito in un'unica foglia /Nums prima che il nuovo range entri, così il label compare davvero nel viewer invece di essere ignorato in silenzio. La vittima tipica è un PDF in stile libro uscito da uno strumento di impaginazione, con numeri romani nel front matter, numerazione araba nel corpo e un'appendice etichettata A-1, A-2, dove volevi solo rinominare l'appendice e non era cambiato nulla
Che cosa sono i page label PDF e come vengono memorizzati?
I page label sono le stringhe che un viewer mostra nel suo box pagine al posto dell'indice di pagina fisico, e ISO 32000-1 §12.4.2 li memorizza come number tree sotto la chiave di catalog /PageLabels. Ogni chiave è un indice di pagina 0-based che apre un range di etichettatura, e ogni valore è un dizionario di page label con fino a tre voci: /S per lo stile di numerazione (D, R, r, A o a), /P per una stringa di prefisso, e /St per il valore numerico della prima pagina del range, che vale 1 per default. Un range corre fino alla chiave successiva, e la specifica richiede che l'albero contenga un valore per l'indice di pagina 0, così ogni pagina è coperta da qualche range
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Pagine 1-4: i, ii, iii, iv (romano minuscolo)
Lib.AddPageLabels(1, 3, 1, '');
// Pagine 5-120: 1, 2, 3 ... (decimale)
Lib.AddPageLabels(5, 1, 1, '');
// Pagine 121 in poi: A-1, A-2 ... (decimale con prefisso)
Lib.AddPageLabels(121, 1, 1, 'A-');
WriteLn(Lib.GetPageLabel(5)); // 1
WriteLn(Lib.GetPageLabel(122)); // A-2
Lib.SaveToFile('handbook-labeled.pdf');
finally
Lib.Free;
end;
end;
TPDFlib.AddPageLabels(Start, Style, Offset, Prefix) mappa i suoi argomenti su quel dizionario senza sorprese una volta note tre regole. Start è 1-based come ogni altro argomento di pagina della libreria e viene scritto nell'albero come Start - 1. Style va da 0 a 5, dove 0 significa solo prefisso e 1 a 5 diventano valori /S D, R, r, A e a; qualunque cosa fuori da quel range restituisce 0 e non tocca nulla. Offset diventa /St solo quando è maggiore di zero, quindi passare 0 si limita a omettere la chiave e il viewer ripiega sul default di 1. Poiché i page label sono arrivati con il PDF 1.3, la chiamata esegue anche EnsureMinVersion('1.3', '/PageLabels'), che alza la versione di output di un file più vecchio a meno che tu abbia bloccato esplicitamente la versione di salvataggio
Perché i nuovi page label spariscono quando l'albero ha /Kids?
I nuovi label spariscono perché ISO 32000-1 §7.9.7 (Table 37) impone che il root di un number tree porti o /Kids o /Nums, mai entrambi, e il vecchio helper NumTreeSet sapeva solo cercare /Nums. I producer che emettono documenti lunghi spesso spezzano l'albero in nodi intermedi, ciascuno con una coppia /Limits, e li appendono a un root che ha solo /Kids. Il vecchio codice non trovava /Nums su quel root, ne creava uno nuovo accanto ai /Kids esistenti, e inseriva lì il nuovo range. Il risultato era un root con due punti d'ingresso mutuamente esclusivi. I viewer scendono attraverso /Kids e non guardano mai l'array smarrito, anche l'EnumNumTree della libreria controlla prima /Kids, e NumTreeLookup rifiuta un nodo dove HasKids xor HasNums è false. AddPageLabels restituiva comunque 1 e il file salvato si apriva comunque pulito, che è il peggior tipo di fallimento: nessuno si lamenta, i label semplicemente restano gli stessi
La correzione in NumTreeSet converte il root in una foglia prima di inserire qualsiasi cosa. Quando il root porta /Kids, EnumNumTree percorre ogni foglia in ordine e raccoglie ogni coppia chiave/valore, un nuovo array piatto /Nums viene costruito da quella lista, e /Kids, /Limits e qualunque /Nums stantio vengono ripuliti dal root prima che l'array piatto venga agganciato. Buttar via /Limits non è cosmetica, dato che la Table 37 consente quella voce solo su nodi intermedi e foglie, mai sul root. Da lì in poi l'inserimento è un ordinario inserimento ordinato in un unico array, e i range esistenti sopravvivono con i loro dizionari di label originali. Il compromesso è voluto: l'albero non viene ricostruito dopo in nodi /Kids bilanciati. Per i page label non costa nulla, perché anche un manuale di riferimento grande raramente ha più di qualche dozzina di range, e una foglia singola è ciò che la maggior parte dei producer scrive comunque
// Rinomina l'appendice in un file il cui root /PageLabels usa /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // es. A-1
// Sostituisci il range che parte dalla pagina 121: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// I range romano e decimale esistenti restano nella foglia appiattita
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, invariato
end;
Come può un array /Nums venire letto male come chiavi?
Un array /Nums viene letto male quando il codice lo percorre un elemento alla volta, perché l'array è una corsa piatta di coppie alternate, [key0 value0 key1 value1 ...], e solo le posizioni pari sono chiavi. Il vecchio loop di NumTreeSet testava ogni elemento per tipo numerico, quindi un valore che per caso era un numero veniva confrontato come se fosse una chiave; un hit minore poteva fissare il punto d'inserimento a un indice dispari e far cadere la nuova coppia in mezzo a una esistente, sbalzando fuori fase ogni coppia successiva. EnumNumTree aveva la stessa camminata a passo singolo. Entrambi ora iterano le coppie con un passo di due, leggendo la chiave a X * 2 e il valore a X * 2 + 1, e una corrispondenza esatta della chiave sostituisce il valore ed esce con Break. In onestà, i valori dei page label sono dizionari, quindi questo secondo bug scattava raramente su /PageLabels in proprio, ma un helper di number tree che legge il passo sbagliato è corrotto nell'istante in cui qualunque valore è numerico, ed è stato sistemato nella stessa passata
Rileggere i label e farli andare in round trip
TPDFlib.GetPageLabel(Page) restituisce il label per una pagina 1-based e ha due fallback che vale la pena conoscere. Senza alcuna voce /PageLabels restituisce il numero di pagina decimale, così chi chiama può usarlo incondizionatamente. Con un albero presente ma nessun range che copre la pagina restituisce una stringa vuota, che è esattamente ciò che accade quando un file salta la voce obbligatoria all'indice 0; la documentazione di riferimento dice che perché i label si visualizzino correttamente deve esistere un range che parte dalla pagina 1, e il codice rende visibile quel requisito. Gli stili lettera seguono la specifica anziché le colonne dei fogli di calcolo: dopo Z arriva AA, poi BB, ripetendo la lettera invece di riportare il riporto
var
P: Integer;
Data: WideString;
begin
// Audit rapido di ciò che un viewer mostrerà nel suo box pagine
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Il valore di opzione 4 esporta solo i range dei label come record PageLabelBegin
Data := Lib.ExportDocumentData(4);
// L'import li rigioca attraverso ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Per le modifiche in blocco, ExportDocumentData con valore di opzione 4 scrive ogni range come blocco PageLabelBegin con righe PageLabelNewIndex, PageLabelStart, PageLabelPrefix e PageLabelNumStyle, e ImportDocumentData tratta il primo record di label che vede come una sostituzione completa: chiama ClearPageLabels una volta e poi passa ogni record a AddPageLabels. Questo rende il round trip testuale deterministico anche quando il file originale usava un albero /Kids, perché la pulizia rimuove l'intera voce di catalog e l'albero ricostruito è una foglia singola dall'inizio
Che cosa la correzione non garantisce ancora?
L'appiattimento è a senso unico e si fida dell'ordine che trova. EnumNumTree raccoglie le coppie in ordine di file, e GetPageLabel applica l'ultimo range la cui chiave è minore o uguale all'indice di pagina, quindi un file straniero le cui foglie sono fuori ordine, che §7.9.7 vieta ma che circola lo stesso, può ancora produrre label sbagliati finché non ricostruisci i range con ClearPageLabels e chiamate fresche di AddPageLabels. I label sono anche legati agli indici di pagina, non agli oggetti pagina, quindi qualunque operazione che cambia il numero o l'ordine delle pagine lascia i range dove sono. Uno scambio sul posto come sostituire pagine preservando i numeri di oggetto mantiene il conteggio e quindi i label allineati, mentre un merge come collare scansioni duplex interfoliate produce una nuova sequenza di pagine che merita un set di range scritto da zero
Le chiamate di page label, la gestione dei number tree e l'export e import dei dati documento descritti qui sono tutti in PDF Library for Delphi per Delphi, C++Builder e Lazarus, con la voce di riferimento di AddPageLabels che documenta i valori di stile e i codici di ritorno