Aggiungete un nuovo intervallo di etichette a un manuale di un fornitore così l'appendice legge App-a, App-b invece di A-1, A-2, salvate, e riaprite il file. Il page box dice ancora A-1. Aprite il catalogo in un editor di testo e il dizionario /PageLabels mostra la ragione: porta un array /Kids ereditato dal producer e, subito accanto, un fresco array /Nums che contiene il vostro intervallo. ISO 32000-1 lascia che la radice di un number tree abbia una di quelle chiavi o l'altra, e un reader che trova /Kids sulla radice ci scende dentro e non guarda oltre
Come un PDF conserva gli intervalli di page label
I page label sono le stringhe che un viewer mostra nel suo page box invece dell'indice fisico della pagina, e ISO 32000-1 §12.4.2 li conserva in un number tree sotto la chiave di catalogo /PageLabels. Ogni chiave è un indice di pagina a base zero dove comincia un intervallo di etichettatura. Ogni valore è un dizionario di etichetta 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 numero della prima pagina dell'intervallo, che vale 1 per default. Un intervallo corre fino alla chiave successiva. La specifica richiede inoltre una voce per l'indice di pagina 0, così ogni pagina del documento cade dentro qualche intervallo
In PDF Library for Delphi lo scrittore è TPDFlib.AddPageLabels(Start, Style, Offset, Prefix), e si mappa su quel dizionario con tre regole che vale la pena memorizzare. Start è a base uno come ogni altro argomento di pagina della libreria, e va nell'albero come Start - 1. Style va da 0 a 5: 0 non scrive affatto uno /S, quindi l'etichetta è il solo prefisso (comodo per una singola pagina etichettata Cover), e 1 a 5 diventano D, R, r, A e a. Uno stile fuori da 0..5 fa restituire 0 alla chiamata e lascia il documento intatto. Offset diventa /St solo quando è maggiore di zero; passate 0 e la chiave viene omessa, il che ogni reader interpreta come 1
Un libro tipico uscito da uno strumento di impaginazione ha bisogno di tre intervalli: numeri romani minuscoli per le pagine preliminari, numeri arabici per il corpo, e un'appendice con prefisso
uses
PDFlibrary;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Pagine 1-4: i, ii, iii, iv (romani minuscoli)
Lib.AddPageLabels(1, 3, 1, '');
// Pagine 5-120: 1, 2, 3 ... (decimali, ripartendo da 1)
Lib.AddPageLabels(5, 1, 1, '');
// Pagine 121 in poi: A-1, A-2 ... (decimali 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;
I page label sono arrivati nel PDF 1.3, quindi ogni chiamata chiede anche al documento di alzare la sua versione di output ad almeno 1.3. Se avete bloccato la versione con LockSaveVersion, quella richiesta viene ignorata e il blocco regge, perché la libreria non passa sopra una versione che avete scelto di proposito. Se dovete bloccare, bloccate a 1.3 o superiore in ogni job che scrive etichette
Perché un nuovo intervallo sparisce quando la radice ha /Kids?
L'intervallo sparisce perché ISO 32000-1 §7.9.7 (Tabella 37) dà alla radice di un number tree o /Kids o /Nums, mai entrambe, e i reader scelgono /Kids quando è presente. I producer che scrivono documenti lunghi spesso spezzano l'albero in nodi intermedi, ciascuno con una coppia /Limits, e li appendono a una radice che ha solo /Kids. Una routine di inserimento che conosce solo /Nums guarda quella radice, non trova alcun array, ne crea uno accanto al /Kids esistente, e ci lascia cadere il nuovo intervallo. La scrittura riesce. Il file resta parsabile. Ogni viewer poi scende per /Kids, vede i tre intervalli originali del producer, e non tocca mai l'array vagante, che è esattamente il sintomo del manuale del fornitore del paragrafo iniziale
La via d'uscita è smettere di trattare la radice come un contenitore che potrebbe far crescere un secondo punto di ingresso. Quando AddPageLabels trova /Kids sulla radice, prima percorre ogni foglio dell'albero esistente da sinistra a destra e raccoglie ogni chiave con il suo dizionario di etichetta, poi costruisce un unico array /Nums piatto da quella lista. /Kids, /Limits e ogni /Nums residuo vengono rimossi dalla radice prima che l'array piatto venga attaccato, e solo allora il nuovo intervallo entra con un normale inserimento ordinato. Rimuovere /Limits conta pure: la Tabella 37 lo ammette solo su nodi intermedi e foglie, e una radice che lo conserva è malformata anche se ora contiene /Nums
Un effetto collaterale è facile da perdere. Se un file vi arriva già rotto, con entrambe le chiavi sulla radice perché qualche strumento precedente ha fatto l'inserimento ingenuo, il percorso dei fogli segue /Kids proprio come farebbe un viewer, quindi gli intervalli dell'array vagante non vengono trascinati dietro. Un viewer non ha mai mostrato nemmeno quegli intervalli, quindi le etichette dopo la modifica corrispondono a ciò che i reader vedevano già
L'albero appiattito non viene ricostruito in nodi /Kids bilanciati in seguito, e riteniamo sia la scelta giusta per questo particolare albero. Il bilanciamento ripaga per i name tree con migliaia di destinazioni o file incorporati. Un albero di page label ha una voce per intervallo di etichettatura, e persino un grosso manuale di riferimento raramente ne richiede più di qualche dozzina; un foglio singolo è anche ciò che la maggior parte dei producer scrive quando non spezzano per il gusto di spezzare. Un passaggio di ribilanciamento aggiungerebbe codice che può rompersi a ogni salvataggio, per un guadagno di lookup che nessuno potrebbe misurare su un albero così piccolo
// Rietichetta l'appendice in un file la cui radice /PageLabels usa /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // per es. A-1
// Stessa pagina di inizio dell'intervallo esistente, quindi il suo valore viene sostituito
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
begin
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, invariato
Lib.SaveToFile('vendor-manual-relabeled.pdf');
end;
end;
Poiché il nuovo intervallo comincia sullo stesso indice di pagina di una chiave esistente, l'inserimento sostituisce il dizionario di quella chiave invece di aggiungere una seconda voce per l'indice di pagina 120. Gli intervalli romano e decimale dai fogli del producer passano attraverso con i loro dizionari originali, compresi gli eventuali valori /St
Il percorso dei fogli deve sopravvivere anche ad alberi malformati. Una voce /Kids che punta indietro a uno dei suoi stessi antenati, o due rami che condividono un foglio, compare nei file usciti da pipeline danneggiate. La traversata ricorda quali nodi ha già visitato, così un ciclo viene percorso una volta e poi lasciato indietro, e un foglio condiviso contribuisce i suoi intervalli una sola volta. Le stesse preoccupazioni per i name tree sono trattate in gestione di cicli nei name tree, /Limits errati e fogli grandi
Percorrere un array /Nums a coppie
Un array /Nums è una sequenza piatta di voci alternate, [key0 value0 key1 value1 ...], e solo le posizioni pari sono chiavi. Quel layout invita a un errore classico. Un ciclo che avanza di un elemento alla volta e chiede "questo è un numero?" confronterà i valori come se fossero chiavi ogni volta che un valore capita essere numerico. Se quel confronto vince un test minore, il punto di inserimento atterra su un indice dispari, la nuova coppia va in mezzo a una esistente, e ogni chiave e valore successivi si scambiano di posto
I valori dei page label sono dizionari, quindi il bug si nasconde su /PageLabels stesso. Viene alla luce il giorno in cui lo stesso helper viene riusato per un number tree i cui valori sono numeri puri, e nulla nella sintassi dell'array vi avvisa di che tipo state tenendo. PDF Library for Delphi legge le chiavi all'indice X * 2 e i valori a X * 2 + 1 sia nel percorso dei fogli sia nell'inserimento, e una corrispondenza esatta di chiave sostituisce il valore e ferma la scansione. Se scrivete codice number tree vostro contro oggetti grezzi, copiate quel passo prima di ogni altra cosa
Rileggere le etichette con GetPageLabel
TPDFlib.GetPageLabel(Page) restituisce l'etichetta per una pagina a base uno, e i suoi due fallback si comportano diversamente di proposito. Un documento senza alcuna voce /PageLabels restituisce il numero di pagina decimale, così un chiamante può usare la funzione incondizionatamente per la visualizzazione. Un documento che ha un albero ma nessun intervallo che copre la pagina richiesta restituisce una stringa vuota. Quel secondo caso è ciò che produce un file che salta la voce obbligatoria per l'indice di pagina 0: le prime pagine non hanno etichetta nel senso della specifica, e una stringa vuota lo riporta più onestamente di quanto farebbe un numero inventato. Iniziate sempre il vostro set di etichette alla pagina 1
Gli stili con lettere seguono la specifica PDF anziché le colonne dei fogli di calcolo. Dopo Z viene AA, poi BB, poi CC; la lettera si ripete invece di riportare, così la pagina 53 di un intervallo A è AAA. I numeri romani sono generati in maiuscolo per R e in minuscolo per r, e il prefisso viene premuto invariato in entrambi i casi
var
P: Integer;
Data: WideString;
begin
// Audit rapido di ciò che un viewer mostrerà nel suo page box
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Il valore di opzione 4 esporta solo gli intervalli di etichette, un blocco
// PageLabelBegin per intervallo
Data := Lib.ExportDocumentData(4);
// ... modificate il testo, o portatelo in un altro file ...
// Il primo record di etichetta ripulisce gli intervalli esistenti, poi ogni
// record viene rieseguito tramite AddPageLabels; restituisce i record applicati
WriteLn(Lib.ImportDocumentData(Data, 4), ' label ranges applied');
end;
Per le modifiche in blocco, ExportDocumentData con valore di opzione 4 scrive ogni intervallo come blocco PageLabelBegin seguito da PageLabelNewIndex (la pagina di inizio a base uno), PageLabelStart, e le righe opzionali PageLabelPrefix e PageLabelNumStyle, usando nomi come LowercaseRomanNumerals e DecimalArabicNumerals. ImportDocumentData tratta il primo record di etichetta che incontra come una sostituzione completa: chiama ClearPageLabels una volta, il che rimuove l'intera voce /PageLabels dal catalogo, e poi passa ogni record a AddPageLabels. L'albero ricostruito è un foglio singolo fin dall'inizio, così un andare e tornare testuale attraverso un originale a forma di /Kids torna nella stessa forma piatta che il percorso di rietichettatura produce
Ordine dei fogli e spostamenti di pagina dopo la rietichettatura
L'appiattimento conserva gli intervalli nell'ordine in cui li trova. Il §7.9.7 richiede chiavi in ordine crescente, ma i file con fogli fuori ordine circolano davvero, e GetPageLabel applica l'ultimo intervallo in quell'ordine la cui chiave è all'indice di pagina o sotto. Per un file che dichiara i suoi intervalli come [0 roman, 120 A-, 4 decimal], con l'intervallo decimale che parte da 1, la pagina 122 si risolve nell'intervallo decimale e legge 118 dove vi aspettavate A-2. Se l'audit pagina per pagina qui sopra mostra etichette del genere, non rammendatele con un'altra chiamata di AddPageLabels. Chiamate ClearPageLabels e riscrivete il set completo in ordine crescente; quattro chiamate costano meno che ragionare su quale intervallo vince
Le etichette sono legate anche agli indici di pagina, non agli oggetti pagina. Qualunque cosa cambi il numero o l'ordine delle pagine lascia gli intervalli esattamente dove erano, puntati alle pagine che ora siedono a quegli indici. Uno scambio sul posto come sostituire pagine preservando i numeri di oggetto conserva il conteggio, quindi le etichette restano allineate. Una fusione come collazione di scansioni duplex interleaved produce una nuova sequenza di pagine, e quell'output dovrebbe ricevere un set di intervalli scritto di fresco dopo la fusione, a partire dalla pagina 1
Le chiamate di etichetta, l'appiattimento di /Kids e l'export e import dei dati documento mostrati qui fanno parte di PDF Library for Delphi, che mira a Delphi, C++Builder e Lazarus; la sua voce di riferimento per AddPageLabels elenca i valori di stile e i codici di ritorno in un'unica tabella