PDF Library for Delphi schrijft paginalabelbereiken met AddPageLabels, en sinds v3.539.10 werkt die aanroep ook op geladen bestanden waarvan de /PageLabels-nummerboom over /Kids-knooppunten is verdeeld: de root wordt afgeplat tot één /Nums-leaf voordat het nieuwe bereik erin gaat, zodat het label ook echt in de viewer verschijnt in plaats van geruisloos genegeerd te worden. Het typische slachtoffer is een boekachtige PDF uit een opmaaktool, met romeinse cijfers in het voorwerk, arabische nummering in de kern en een appendix gelabeld A-1, A-2, waar u alleen de appendix opnieuw wilde labelen en er precies niets veranderde
Wat zijn PDF-paginalabels en hoe worden ze opgeslagen?
Paginalabels zijn de strings die een viewer in zijn paginabox toont in plaats van de fysieke pagina-index, en ISO 32000-1 §12.4.2 slaat ze op als een number tree onder de catalogussleutel /PageLabels. Elke sleutel is een 0-based pagina-index die een labelbereik opent, en elke waarde is een page-label-dictionary met hoogstens drie entries: /S voor de nummeringsstijl (D, R, r, A of a), /P voor een prefixstring, en /St voor de numerieke waarde van de eerste pagina in het bereik, met 1 als default. Een bereik loopt tot de volgende sleutel, en de specificatie eist dat de boom een waarde voor pagina-index 0 bevat, dus elke pagina valt onder een of ander bereik
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Pagina's 1-4: i, ii, iii, iv (kleine romeinse cijfers)
Lib.AddPageLabels(1, 3, 1, '');
// Pagina's 5-120: 1, 2, 3 ... (decimaal)
Lib.AddPageLabels(5, 1, 1, '');
// Vanaf pagina 121: A-1, A-2 ... (decimaal met een 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) landt zijn argumenten zonder verrassingen op die dictionary zodra u drie regels kent. Start is 1-based zoals elk ander pagina-argument in de library en wordt als Start - 1 in de boom weggeschreven. Style loopt van 0 tot 5, waarbij 0 alleen-prefix betekent en 1 tot 5 de /S-waarden D, R, r, A en a worden; alles buiten dat bereik geeft 0 terug en raakt niets aan. Offset wordt alleen /St als hij groter dan nul is, dus een 0 doorgeven laat de sleutel gewoon weg en de viewer valt terug op de default van 1. Omdat paginalabels met PDF 1.3 kwamen, draait de aanroep ook EnsureMinVersion('1.3', '/PageLabels'), wat de uitvoerversie van een ouder bestand verhoogt tenzij u de save-versie expliciet heeft vastgezet
Waarom verdwijnen nieuwe paginalabels als de boom /Kids heeft?
Nieuwe labels verdwijnen omdat ISO 32000-1 §7.9.7 (Tabel 37) verplicht dat de root van een number tree ofwel /Kids ofwel /Nums draagt, nooit beide, en de oudere helper NumTreeSet alleen wist waar hij /Nums vinden moest. Producenten van lange documenten splitsen de boom vaak in tussenknooppunten, elk met een /Limits-paar, en hangen die aan een root die alleen /Kids heeft. De oude code vond geen /Nums op die root, maakte een verse naast de bestaande /Kids aan, en zette het nieuwe bereik daar in. Het resultaat was een root met twee elkaar uitsluitende toegangspunten. Viewers dalen af via /Kids en kijken nooit naar de verdwaalde array, de eigen EnumNumTree van de library controleert ook eerst /Kids, en NumTreeLookup weigert een knooppunt waar HasKids xor HasNums onwaar is. AddPageLabels gaf nog steeds 1 terug en het opgeslagen bestand opende nog steeds keurig, en dat is de ergste soort falen: niets klaagt, de labels blijven gewoon hetzelfde
De fix in NumTreeSet maakt van de root een leaf voordat er iets wordt ingevoegd. Draagt de root /Kids, dan loopt EnumNumTree elke leaf in volgorde af en verzamelt elk sleutel-waarde-paar, er wordt een nieuwe vlakke /Nums-array uit die lijst gebouwd, en /Kids, /Limits en een eventuele achtergebleven /Nums worden uit de root gezuiverd voordat de vlakke array wordt vastgezet. /Limits laten vallen is geen cosmetica, want Tabel 37 staat die entry alleen op tussen- en leaf-knooppunten toe, nooit op de root. Vanaf dat moment is het invoegen een gewone gesorteerde insert in één array, en bestaande bereiken overleven met hun originele labeldictionaries. De trade-off is bewust: de boom wordt daarna niet opnieuw opgebouwd als gebalanceerde /Kids-knooppunten. Voor paginalabels kost dat niets, want zelfs een groot naslagwerk heeft zelden meer dan enkele tientallen bereiken, en één enkele leaf is toch al wat de meeste producenten schrijven
// Label de appendix opnieuw in een bestand waarvan de /PageLabels-root /Kids gebruikt
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // bv. A-1
// Vervang het bereik dat op pagina 121 begint: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Bestaande romeinse en decimale bereiken zitten nog in de afgeplatte leaf
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, onveranderd
end;
Hoe wordt een /Nums-array verkeerd gelezen als sleutels?
Een /Nums-array wordt verkeerd gelezen wanneer code haar één element per keer afgaat, want de array is een vlakke reeks afwisselende paren, [key0 value0 key1 value1 ...], en alleen de even posities zijn sleutels. De oude NumTreeSet-lus toonde elk element op numeriek type, dus een waarde die toevallig een getal was werd vergeleken alsof het een sleutel was; een kleiner-dan-treffer kon het invoegpunt op een oneven index zetten en het nieuwe paar midden in een bestaand paar droppen, waardoor alle latere paren uit de pas liepen. EnumNumTree had dezelfde eenstaps-walk. Allebei itereren ze nu over paren met een pas van twee, met de sleutel op X * 2 en de waarde op X * 2 + 1, en een exacte sleutelmatch vervangt de waarde en verlaat de lus met Break. Eerlijkheidshalve: paginalabelwaarden zijn dictionaries, dus deze tweede bug ging op /PageLabels zelf zelden af, maar een number-tree-helper die de verkeerde pas leest is corrupt op het moment dat enige waarde numeriek is, en die is in dezelfde passage gefixt
Labels teruglezen en er een round trip van maken
TPDFlib.GetPageLabel(Page) geeft het label voor een 1-based pagina terug en heeft twee fallbacks die u wilt kennen. Zonder enige /PageLabels-entry geeft hij het decimale paginanummer terug, dus een aanroeper kan hem zonder voorwaarden gebruiken. Met een boom maar zonder bereik dat de pagina dekt geeft hij een lege string terug, wat precies gebeurt wanneer een bestand de verplichte index-0-entry overslaat; de referencedocumentatie zegt dat er een bereik dat op pagina 1 begint moet bestaan opdat labels correct tonen, en de code maakt die eis zichtbaar. Letterstijlen volgen de specificatie in plaats van spreadsheetkolommen: na Z komt AA, dan BB, met herhaling van de letter in plaats van overdracht
var
P: Integer;
Data: WideString;
begin
// Snelle audit van wat een viewer in zijn paginabox toont
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Optiewaarde 4 exporteert alleen labelbereiken als PageLabelBegin-records
Data := Lib.ExportDocumentData(4);
// Importeren spoelt ze opnieuw af via ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Voor bulkbewerkingen schrijft ExportDocumentData met optiewaarde 4 elk bereik weg als een PageLabelBegin-blok met de regels PageLabelNewIndex, PageLabelStart, PageLabelPrefix en PageLabelNumStyle, en ImportDocumentData behandelt de eerste labelrecord die hij ziet als volledige vervanging: hij roept één keer ClearPageLabels aan en voert daarna elke record aan AddPageLabels. Daarmee is een text round trip deterministisch, ook wanneer het originele bestand een /Kids-boom gebruikte, want het wissen verwijdert de hele catalogusentry en de herbouwde boom is vanaf het begin één leaf
Wat garandeert de fix nog steeds niet?
Het afplatten is eenrichtingsverkeer en vertrouwt de volgorde die hij aantreft. EnumNumTree verzamelt paren in bestandsvolgorde, en GetPageLabel past het laatste bereik toe waarvan de sleutel kleiner dan of gelijk aan de pagina-index is, dus een buitenlands bestand waarvan de leafs uit de pas lopen, wat §7.9.7 verbiedt maar wat wel degelijk circuleert, kan nog steeds verkeerde labels opleveren tot u de bereiken herbouwt met ClearPageLabels en verse AddPageLabels-aanroepen. Labels zijn bovendien gebonden aan pagina-indexen, niet aan pagina-objecten, dus elke operatie die het paginanaantal of de volgorde verandert laat de bereiken achter waar ze stonden. Een in-place swap zoals pagina's vervangen met behoud van objectnummers houdt het aantal en daarmee de labels uitgelijnd, terwijl een merge zoals het samenvoegen van interleaved duplexscans een nieuwe paginavolgorde oplevert die een vers geschreven set bereiken verdient
De paginalabel-aanroepen, de number-tree-verwerking en de documentdata-export en -import die hier zijn beschreven zitten allemaal in PDF Library for Delphi voor Delphi, C++Builder en Lazarus, met de referentie-entry van AddPageLabels als documentatie van de stijlwaarden en retourcodes