PDF Library for Delphi skriver page label-ranges med AddPageLabels, og siden v3.539.10 virker det kald også på loadede filer, hvis /PageLabels number tree er splittet op i /Kids-noder: roden flattes ud til ét enkelt /Nums-leaf, før den nye range kommer ind, så labelen rent faktisk vises i vieweren i stedet for lydløst at blive ignoreret. Det typiske offer er en bog-agtig PDF fra et layoutværktøj, med romertal i forordet, arabiske numre i hoveddelen og et appendiks mærket A-1, A-2, hvor du bare ville omdøbe appendikset, og der skete ingenting
Hvad er PDF page labels, og hvordan gemmes de?
Page labels er de strenge, en viewer viser i sin sideboks i stedet for det fysiske sideindeks, og ISO 32000-1 §12.4.2 gemmer dem som et number tree under catalog-nøglen /PageLabels. Hver nøgle er et 0-baseret sideindeks, der starter en labeling-range, og hver værdi er en page label-dictionary med op til tre entries: /S for numreringsstilen (D, R, r, A eller a), /P for en prefix-streng og /St for den numeriske værdi af første side i rangen, som default er 1. En range løber til næste nøgle, og specifikationen kræver, at træet indeholder en værdi for sideindeks 0, så hver side er dækket af en range
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Sider 1-4: i, ii, iii, iv (små romertal)
Lib.AddPageLabels(1, 3, 1, '');
// Sider 5-120: 1, 2, 3 ... (decimal)
Lib.AddPageLabels(5, 1, 1, '');
// Sider 121 og frem: A-1, A-2 ... (decimal med et 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) mapper sine argumenter over på den dictionary uden overraskelser, når du kender tre regler. Start er 1-baseret som alle andre sideargumenter i biblioteket og skrives ind i træet som Start - 1. Style løber fra 0 til 5, hvor 0 betyder kun prefix og 1 til 5 bliver til /S-værdierne D, R, r, A og a; alt uden for det interval returnerer 0 og rører ingenting. Offset bliver til /St kun, når den er større end nul, så at give 0 udelader simpelthen nøglen, og vieweren falder tilbage til default 1. Fordi page labels kom med i PDF 1.3, kører kaldet også EnsureMinVersion('1.3', '/PageLabels'), som hæver outputversionen af en ældre fil, medmindre du eksplicit har låst save-versionen
Hvorfor forsvinder nye page labels, når træet har /Kids?
Nye labels forsvinder, fordi ISO 32000-1 §7.9.7 (Tabel 37) gør, at roden af et number tree bærer enten /Kids eller /Nums, aldrig begge, og den tidligere NumTreeSet-helper kunne kun lede efter /Nums. Producenter, der spytter lange dokumenter ud, splitter ofte træet op i intermediære noder, hver med et /Limits-par, og hænger dem under en rod, der kun har /Kids. Den gamle kode fandt ingen /Nums på den rod, oprettede en frisk ved siden af de eksisterende /Kids og indsatte den nye range der. Resultatet var en rod med to gensidigt udelukkende indgangspunkter. Viewers går ned gennem /Kids og ser aldrig på den forældreløse array, bibliotekets egen EnumNumTree tjekker også /Kids først, og NumTreeLookup afviser en node, hvor HasKids xor HasNums er false. AddPageLabels returnerede stadig 1, og den gemte fil åbnede stadig rent, hvilket er den værst tænkelige slags fejl: intet brokker sig, labels forbliver bare de samme
Fixet i NumTreeSet konverterer roden til et leaf, før noget indsættes. Når roden bærer /Kids, går EnumNumTree hvert leaf igennem i rækkefølge og samler hvert nøgle-værdi-par, et nyt fladt /Nums-array bygges ud fra den liste, og /Kids, /Limits og eventuelle forældede /Nums renses ud af roden, før det flade array hænges på. At droppe /Limits er ikke kosmetik, for Tabel 37 tillader den entry kun på intermediære og leaf-noder, aldrig på roden. Fra det punkt er indsættelsen en almindelig sorteret indsættelse i ét array, og eksisterende ranges overlever med deres originale label-dictionaries. Trade-offet er bevidst: træet genopbygges ikke bagefter i balancerede /Kids-noder. For page labels koster det ingenting, for selv en stor referencehåndbog har sjældent mere end et par dusin ranges, og et enkelt leaf er, hvad de fleste producenter skriver alligevel
// Relabel appendikset i en fil, hvis /PageLabels-rod bruger /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // fx A-1
// Erstat rangen, der starter på side 121: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Eksisterende romerske og decimale ranges ligger stadig i det flattede leaf
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, uændret
end;
Hvordan kan et /Nums-array fejllæses som nøgler?
Et /Nums-array fejllæses, når koden gennemløber det ét element ad gangen, for arrayet er en flad række af alternerende par, [key0 value0 key1 value1 ...], og kun de lige positioner er nøgler. Den gamle NumTreeSet-løkke testede hvert element for en numerisk type, så en værdi, der tilfældigvis var et tal, blev sammenlignet, som om den var en nøgle; et less-than-hit kunne sætte indsættelsespunktet til et ulige indeks og droppe det nye par midt i et eksisterende, så alle senere par kom ud af trit. EnumNumTree havde samme enkelt-trins gennemløb. Begge itererer nu par med en stride på to, læser nøglen ved X * 2 og værdien ved X * 2 + 1, og et eksakt nøgle-match erstatter værdien og afslutter med Break. I retfærdighedens navn er page label-værdier dictionaries, så denne anden bug udløstes sjældent på /PageLabels selv, men en number-tree-helper, der læser forkert stride, er korrupt i det øjeblik, en værdi er numerisk, og den blev fikset i samme omgang
At læse labels tilbage og round-trippe dem
TPDFlib.GetPageLabel(Page) returnerer labelen for en 1-baseret side og har to fallbacks, der er værd at kende. Uden nogen /PageLabels-entry returnerer den decimal-sidenummeret, så en caller kan bruge den betingelsesløst. Med et træ til stede, men ingen range, der dækker siden, returnerer den en tom streng, hvilket præcis sker, når en fil springer den obligatoriske indeks 0-entry over; referencedokumentationen siger, at en range, der starter på side 1, skal findes, for at labels kan vises korrekt, og koden gør det krav synligt. Bogstav-stile følger specifikationen snarere end regnearkskolonner: efter Z kommer AA, derefter BB, som gentager bogstavet i stedet for at rulle videre til næste
var
P: Integer;
Data: WideString;
begin
// Hurtig gennemgang af, hvad en viewer viser i sin sideboks
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Optionværdi 4 eksporterer kun label-ranges som PageLabelBegin-records
Data := Lib.ExportDocumentData(4);
// Import afspiller dem igen gennem ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Til bulk-redigeringer skriver ExportDocumentData med optionværdi 4 hver range som en PageLabelBegin-blok med PageLabelNewIndex-, PageLabelStart-, PageLabelPrefix- og PageLabelNumStyle-linjer, og ImportDocumentData behandler den første label-record, den ser, som en fuldstændig erstatning: den kalder ClearPageLabels én gang og fodrer derefter hver record til AddPageLabels. Det gør en tekst-round-trip deterministisk, selv når den originale fil brugte et /Kids-træ, for clearing fjerner hele catalog-entryen, og det genopbyggede træ er ét enkelt leaf fra starten
Hvad garanterer fixet stadig ikke?
Flatteningen er ensrettet og stoler på den rækkefølge, den finder. EnumNumTree samler par i filrækkefølge, og GetPageLabel anvender den sidste range, hvis nøgle er mindre end eller lig sideindekset, så en fremmed fil, hvis leaves er ude af rækkefølge — noget §7.9.7 forbyder, men som cirkulerer derude — stadig kan give forkerte labels, til du genopbygger ranges med ClearPageLabels og friske AddPageLabels-kald. Labels er også bundet til sideindekser, ikke sideobjekter, så enhver operation, der ændrer sideantal eller rækkefølge, efterlader ranges, hvor de var. Et in-place-swap som udskiftning af sider under bevarelse af objektnumre holder antallet og dermed labels justeret, mens en merge som sammenfletning af interleaved duplex-scanninger producerer en ny siderækkefølge, der fortjener et nyskrevet sæt ranges
Page label-kaldene, number tree-håndteringen og den document data-eksport og -import, der er beskrevet her, følger alle med i PDF Library for Delphi til Delphi, C++Builder og Lazarus, med reference-entryen for AddPageLabels, som dokumenterer stilværdierne og returkoderne