Teknisk artikel

PDF page labels i Delphi: reparation af /Kids number trees

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

Page label-opbevaring i PDFlibPas-termer: /PageLabels number tree nøgler hver range efter dens 0-baserede startside, hver værdi er en label-dictionary med /S-stil, /P-prefix og /St-første nummer, og bogeksemplet mapper romersk forord, arabiske hoveddelssider og et A- appendiks på tre ranges
En range løber til næste nøgle, specifikationen kræver en værdi for sideindeks 0, og GetPageLabel anvender den sidste range, hvis nøgle ligger på eller under siden, så hver side resolver til noget
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

Number tree-reparation i PDFlibPas: en rod, der bærer /Kids og et forældreløst /Nums-array, er usynlig for viewers, fordi ISO 32000-1 kun tillader én af de to, så NumTreeSet flatter hvert leaf ud til ét enkelt /Nums-array og renses /Kids og /Limits ud, som Tabel 37 aldrig tillader på en rod
Intet brokkede sig, fordi alle tjek bestod: AddPageLabels returnerede 1, den gemte fil åbnede rent, og kun en reader, der først går ned gennem /Kids, sådan som både viewers og biblioteket selv gør, finder aldrig den nye range
// 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

Pair-stride-fix i PDFlibPas number trees: et /Nums-array er en flad række af alternerende nøgle- og værdi-entries, så et gennemløb, der tester hvert element, kunne indsætte et nyt par ved et ulige indeks og forskubbe senere par, mens det rettede gennemløb læser nøglen ved X*2 og værdien ved X*2+1
Buggen udløstes sjældent på /PageLabels, fordi label-værdier er dictionaries, men en number-tree-helper, der læser forkert stride, bliver korrupt i det øjeblik, en værdi er numerisk, så begge gennemløb træder nu i par

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