PDF Library for Delphi schreibt Page-Label-Bereiche mit AddPageLabels, und seit v3.539.10 funktioniert dieser Aufruf auch auf geladenen Dateien, deren /PageLabels-Number-Tree in /Kids-Knoten aufgeteilt ist: Die Wurzel wird zu einem einzigen /Nums-Blatt flach gemacht, bevor der neue Bereich hineinkommt, sodass das Label tatsächlich im Viewer auftaucht, statt stillschweigend ignoriert zu werden. Das typische Opfer ist ein Buch-PDF aus einem Layout-Tool, mit römischen Ziffern im Vorspann, arabischer Nummerierung im Hauptteil und einem Anhang mit A-1, A-2 – Sie wollten nur den Anhang umlabeln, und nichts veränderte sich
Was sind PDF-Page-Labels und wie werden sie gespeichert?
Page Labels sind die Zeichenketten, die ein Viewer in seiner Seitenbox zeigt, statt des physischen Seitenindex, und ISO 32000-1 §12.4.2 speichert sie als Number Tree unter dem Katalog-Key /PageLabels. Jeder Key ist ein 0-basierter Seitenindex, der einen Labeling-Bereich startet, und jeder Wert ist ein Page-Label-Dictionary mit bis zu drei Einträgen: /S für den Nummerierungsstil (D, R, r, A oder a), /P für einen Prefix-String und /St für den numerischen Wert der ersten Seite im Bereich, der standardmäßig 1 ist. Ein Bereich läuft bis zum nächsten Key, und die Spezifikation verlangt, dass der Tree einen Wert für Seitenindex 0 enthält, sodass jede Seite von irgendeinem Bereich abgedeckt ist
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Seiten 1-4: i, ii, iii, iv (kleine römische Ziffern)
Lib.AddPageLabels(1, 3, 1, '');
// Seiten 5-120: 1, 2, 3 ... (dezimal)
Lib.AddPageLabels(5, 1, 1, '');
// Seiten ab 121: A-1, A-2 ... (dezimal mit Prefix)
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) mappt seine Argumente ohne Überraschungen auf dieses Dictionary, sobald Sie drei Regeln kennen. Start ist 1-basiert wie jedes andere Seitenargument in der Bibliothek und wird als Start - 1 in den Tree geschrieben. Style läuft von 0 bis 5, wobei 0 nur den Prefix bedeutet und 1 bis 5 zu /S-Werten D, R, r, A und a werden; alles außerhalb davon gibt 0 zurück und fasst nichts an. Offset wird nur dann zu /St, wenn es größer als null ist, bei 0 wird der Key einfach weggelassen und der Viewer fällt auf den Default 1 zurück. Weil Page Labels erst in PDF 1.3 kamen, führt der Aufruf auch EnsureMinVersion('1.3', '/PageLabels') aus, was die Ausgabeversion einer älteren Datei anhebt, sofern Sie die Save-Version nicht explizit festgenagelt haben
Warum verschwinden neue Page Labels, wenn der Tree /Kids hat?
Weil ISO 32000-1 §7.9.7 (Tabelle 37) festlegt, dass die Wurzel eines Number Trees entweder /Kids oder /Nums trägt, nie beides, und der frühere NumTreeSet-Helfer nur nach /Nums suchte. Producer, die lange Dokumente ausgeben, teilen den Tree oft in Zwischenknoten, jeweils mit einem /Limits-Paar, und hängen sie an eine Wurzel, die nur /Kids hat. Der alte Code fand keine /Nums an dieser Wurzel, legte ein frisches neben das bestehende /Kids und steckte den neuen Bereich dort hinein. Das Ergebnis war eine Wurzel mit zwei sich gegenseitig ausschließenden Einstiegspunkten. Viewer steigen über /Kids hinab und schauen das verirrte Array nie an, auch der eigene EnumNumTree der Bibliothek prüft zuerst /Kids, und NumTreeLookup verweigert einen Knoten, bei dem HasKids xor HasNums falsch ist. AddPageLabels gab trotzdem 1 zurück, und die gespeicherte Datei öffnete trotzdem sauber – das ist die schlimmste Art des Scheiterns: nichts beschwert sich, die Labels bleiben einfach dieselben
Der Fix in NumTreeSet macht die Wurzel zu einem Blatt, bevor irgendetwas eingefügt wird. Trägt die Wurzel /Kids, läuft EnumNumTree jedes Blatt der Reihe nach ab und sammelt jedes Key-Value-Paar ein, aus dieser Liste wird ein neues flaches /Nums-Array gebaut, und /Kids, /Limits und veraltete /Nums werden aus der Wurzel entfernt, bevor das flache Array angehängt wird. Das Wegwerfen von /Limits ist nicht kosmetisch, denn Tabelle 37 erlaubt diesen Eintrag nur an Zwischen- und Blattknoten, nie an der Wurzel. Von da an ist das Einfügen ein gewöhnlicher sortierter Insert in ein Array, und bestehende Bereiche überleben mit ihren Original-Label-Dictionaries. Der Trade-off ist Absicht: Der Tree wird danach nicht in balancierte /Kids-Knoten zurückgebaut. Bei Page Labels kostet das nichts, denn selbst ein großes Referenzhandbuch hat selten mehr als ein paar Dutzend Bereiche, und ein einziges Blatt ist es, was die meisten Producer ohnehin schreiben
// Den Anhang umlabeln in einer Datei, deren /PageLabels-Wurzel /Kids nutzt
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // z. B. A-1
// Den Bereich ab Seite 121 ersetzen: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Bestehende römische und dezimale Bereiche bleiben im flachen Blatt
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, unverändert
end;
Wie kann ein /Nums-Array als Keys fehlgelesen werden?
Ein /Nums-Array wird fehlgelesen, wenn Code es Element für Element abläuft, denn das Array ist ein flacher Zug abwechselnder Paare, [key0 value0 key1 value1 ...], und nur die geraden Positionen sind Keys. Die alte NumTreeSet-Schleife testete jedes Element auf einen numerischen Typ, also wurde ein Wert, der zufällig eine Zahl war, verglichen, als wäre er ein Key; ein Kleiner-Treffer konnte den Insert-Punkt auf einen ungeraden Index setzen und das neue Paar mitten in ein bestehendes werfen, sodass alle späteren Paare aus dem Takt gerieten. EnumNumTree hatte denselben Einzelschritt-Walk. Beide iterieren jetzt Paare mit Schrittweite zwei, lesen den Key bei X * 2 und den Wert bei X * 2 + 1, und ein exakter Key-Treffer ersetzt den Wert und steigt mit Break aus. Um fair zu bleiben: Page-Label-Werte sind Dictionaries, dieser zweite Bug feuerte auf /PageLabels selbst also selten, aber ein Number-Tree-Helfer, der die falsche Schrittweite liest, ist korrumpiert, sobald irgendein Wert numerisch ist – und er wurde im selben Durchgang gefixt
Labels zurücklesen und als Round Trip führen
TPDFlib.GetPageLabel(Page) gibt das Label für eine 1-basierte Seite zurück und hat zwei Fallbacks, die man kennen sollte. Ohne jeglichen /PageLabels-Eintrag gibt es die dezimale Seitennummer zurück, ein Aufrufer kann es also bedenkenlos immer verwenden. Mit vorhandenem Tree, aber keinem Bereich, der die Seite abdeckt, gibt es einen leeren String zurück – genau das passiert, wenn eine Datei den zwingenden Index-0-Eintrag überspringt; die Referenzdokumentation sagt, dass ein Bereich ab Seite 1 existieren muss, damit Labels korrekt angezeigt werden, und der Code macht diese Anforderung sichtbar. Buchstaben-Stile folgen der Spezifikation, nicht Tabellenkalkulations-Spalten: Nach Z kommt AA, dann BB – der Buchstabe wiederholt sich, statt zu übertragen
var
P: Integer;
Data: WideString;
begin
// Kurzer Audit dessen, was ein Viewer in seiner Seitenbox zeigen wird
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Optionswert 4 exportiert nur Label-Bereiche als PageLabelBegin-Records
Data := Lib.ExportDocumentData(4);
// Der Import spielt sie über ClearPageLabels + AddPageLabels ab
Lib.ImportDocumentData(Data, 0);
end;
Für Massen-Edits schreibt ExportDocumentData mit Optionswert 4 jeden Bereich als PageLabelBegin-Block mit PageLabelNewIndex-, PageLabelStart-, PageLabelPrefix- und PageLabelNumStyle-Zeilen, und ImportDocumentData behandelt den ersten Label-Record, den es sieht, als vollständige Ersetzung: Es ruft einmal ClearPageLabels auf und füttert dann jeden Record an AddPageLabels. Das macht einen Text-Round-Trip deterministisch, selbst wenn die Originaldatei einen /Kids-Tree hatte, denn das Leeren entfernt den kompletten Katalog-Eintrag, und der neu gebaute Tree ist von Anfang an ein einziges Blatt
Was garantiert der Fix weiterhin nicht?
Das Flachmachen ist eine Einbahnstraße und vertraut der Reihenfolge, die es vorfindet. EnumNumTree sammelt Paare in Dateireihenfolge ein, und GetPageLabel wendet den letzten Bereich an, dessen Key kleiner oder gleich dem Seitenindex ist, also kann eine Fremddatei, deren Blätter out of order sind – §7.9.7 verbietet das, aber solche Dateien zirkulieren – weiterhin falsche Labels liefern, bis Sie die Bereiche mit ClearPageLabels und frischen AddPageLabels-Aufrufen neu bauen. Labels hängen auch an Seitenindizes, nicht an Seitenobjekten, also lässt jede Operation, die Seitenzahl oder -ordnung ändert, die Bereiche dort, wo sie waren. Ein In-place-Tausch wie Seiten ersetzen bei erhaltenen Objektnummern hält die Zahl und damit die Labels ausgerichtet, während ein Merge wie das Zusammenführen verschränkter Duplex-Scans eine neue Seitenfolge produziert, die einen frisch geschriebenen Satz Bereiche verdient
Die hier beschriebenen Page-Label-Aufrufe, das Number-Tree-Handling und der Document-Data-Export und -Import erscheinen alle in der PDF Library for Delphi für Delphi, C++Builder und Lazarus, mit dem Referenzeintrag zu AddPageLabels, der die Stilwerte und Rückgabecodes dokumentiert