Tehnički članak

Oznake stranica u Delphiju: popravak /Kids number treeova

PDF Library for Delphi zapisuje raspone oznaka stranica metodom AddPageLabels, i od v3.539.10 taj poziv radi i na učitanim datotekama čiji je /PageLabels number tree raspršen u /Kids čvorove: korijen se izravna u jedan /Nums leaf prije nego novi raspon uđe, pa oznaka stvarno iskoči u pregledniku umjesto da tiho bude ignorirana. Tipična žrtva je PDF u stilu knjige iz layout alata, s rimskim brojevima u prednjem materijalu, arapskim numeriranjem u tijelu i prilogom označenim A-1, A-2, gdje ste željeli preoznačiti samo prilog — i ništa se nije promijenilo

Što su PDF oznake stranica i kako se spremaju?

Oznake stranica stringovi su koje preglednik pokazuje u svojoj kutiji stranice umjesto fizičkog indeksa stranice, a ISO 32000-1 §12.4.2 sprema ih kao number tree pod ključem kataloga /PageLabels. Svaki je ključ indeks stranice od nule koji počinje raspon označavanja, a svaka je vrijednost rječnik oznake stranica s najviše tri unosa: /S za stil numeriranja (D, R, r, A ili a), /P za prefiks string i /St za brojčanu vrijednost prve stranice u rasponu, koja po zadanom je 1. Raspon traje do sljedećeg ključa, a specifikacija zahtijeva da tree sadrži vrijednost za indeks stranice 0, pa je svaka stranica pokrivena nekim rasponom

Pohrana oznaka stranica u pojmovima PDFlibPasa: /PageLabels number tree ključa svaki raspon nultim početnim indeksom stranice, svaka je vrijednost rječnik oznake sa /S stilom, /P prefiksom i /St prvim brojem, a knjižni primjer preslikava rimski prednji materijal, arapske stranice tijela i A- prilog na tri raspona
Raspon traje do sljedećeg ključa, specifikacija zahtijeva vrijednost za indeks stranice 0, a GetPageLabel primjenjuje zadnji raspon čiji je ključ na stranici ili ispod nje, pa se svaka stranica razriješi u nešto
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
      Exit;
    // Stranice 1-4: i, ii, iii, iv (mala rimska)
    Lib.AddPageLabels(1, 3, 1, '');
    // Stranice 5-120: 1, 2, 3 ... (decimalna)
    Lib.AddPageLabels(5, 1, 1, '');
    // Stranice 121 dalje: A-1, A-2 ... (decimalna s prefiksom)
    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) preslikava svoje argumente na taj rječnik bez iznenađenja jednom kad znate tri pravila. Start kreće od 1, kao i svaki drugi argument stranice u biblioteci, i u tree se zapisuje kao Start - 1. Style ide od 0 do 5, gdje 0 znači samo prefiks, a 1 do 5 postaju /S vrijednosti D, R, r, A i a; sve izvan tog raspona vraća 0 i ništa ne dira. Offset postaje /St samo kad je veći od nule, pa 0 jednostavno izostavlja ključ i preglednik se vraća na zadanu jedinicu. Budući da su oznake stranica stigle s PDF 1.3, poziv pokreće i EnsureMinVersion('1.3', '/PageLabels'), koji podigne izlaznu verziju starije datoteke osim ako ste verziju spremanja izričito zaključali

Zašto nove oznake stranica nestaju kad tree ima /Kids?

Nove oznake nestaju jer ISO 32000-1 §7.9.7 (Tablica 37) traži da korijen number treea nosi ili /Kids ili /Nums, nikad oboje, a raniji helper NumTreeSet znao je samo tražiti /Nums. Producenti dugih dokumenata često rasprsnu tree u međučvorove, svaki s parom /Limits, i objese ih na korijen koji ima samo /Kids. Stari kod na tom korijenu nije našao /Nums, stvorio je svjež uz postojeće /Kids, i tamo umetnuo novi raspon. Rezultat je bio korijen s dva međusobno isključiva ulaza. Preglednici silaze kroz /Kids i nikad ne pogledaju zalutalo polje, i bibliotečin vlastiti EnumNumTree prvo provjerava /Kids, a NumTreeLookup odbija čvor u kojem HasKids xor HasNums nije istinito. AddPageLabels i dalje je vratio 1, a spremljena se datoteka i dalje uredno otvorila, što je najgora vrsta kvara: ništa ne prigovara, oznake jednostavno ostaju iste

Popravak u NumTreeSet pretvara korijen u leaf prije nego išta umetne. Kad korijen nosi /Kids, EnumNumTree obiđe svaki leaf redom i skupi svaki par ključ-vrijednost, iz tog popisa sagradi se novo ravno /Nums polje, a /Kids, /Limits i svaki ustajali /Nums isčiste se iz korijena prije nego se ravno polje prikači. Izbacivanje /Limits nije kozmetika, jer Tablica 37 dopušta taj unos samo na međučvorovima i leafovima, nikad na korijenu. Od tog trenutka umetanje je obično sortirano ubacivanje u jedno polje, a postojeći rasponi prežive sa svojim izvornim rječnicima oznaka. Kompromis je namjeran: tree se poslije ne gradi iznova u balansirane /Kids čvorove. Za oznake stranica to ne košta ništa, jer čak i veliki priručnik rijetko ima više od nekoliko desetaka raspona, a jedan je leaf ono što većina producenata ionako i zapiše

Popravak number treea u PDFlibPasu: korijen koji nosi /Kids i zalutalo /Nums polje nevidljiv je preglednicima jer ISO 32000-1 dopušta samo jedno od dvaju, pa NumTreeSet izravna svaki leaf u jedno /Nums polje i isčisti /Kids i /Limits, što Tablica 37 nikad ne dopušta na korijenu
Ništa nije prigovorilo jer je svaka provjera prošla: AddPageLabels vratio je 1, spremljena se datoteka uredno otvorila, a samo čitač koji prvo silazi kroz /Kids, kako čine i preglednici i sama biblioteka, nikad ne nađe novi raspon
// Preoznači prilog u datoteci čiji /PageLabels korijen koristi /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
  WriteLn('Before: ', Lib.GetPageLabel(121));  // npr. A-1
  // Zamijeni raspon koji počinje na stranici 121: App-a, App-b ...
  if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
    Lib.SaveToFile('vendor-manual-relabeled.pdf');
  // Postojeći rimski i decimalni rasponi i dalje su u izravnanom leafu
  WriteLn('After: ', Lib.GetPageLabel(121));   // App-a
  WriteLn('Front: ', Lib.GetPageLabel(2));     // ii, nepromijenjeno
end;

Kako se /Nums polje može pogrešno pročitati kao ključevi?

/Nums se polje pogrešno pročita kad ga kod prelazi element po element, jer je polje ravno niz izmjeničnih parova, [key0 value0 key1 value1 ...], i samo su parni položaji ključevi. Stara NumTreeSet petlja testirala je svaki element za brojčani tip, pa je vrijednost koja je slučajno bila broj uspoređivana kao da je ključ; pogodak manje-od mogao je postaviti točku umetanja na neparni indeks i baciti novi par usred postojećeg, pomicući svaki kasniji par iz takta. EnumNumTree imao je isti jedno-koračni obilazak. Oba sada iteriraju parovima s korakom dva, čitajući ključ na X * 2 i vrijednost na X * 2 + 1, i točno poklapanje ključa zamijeni vrijednost i izađe s Break. Da budemo pravedni, vrijednosti oznaka stranica su rječnici, pa je ovaj drugi bug na samom /PageLabels rijetko opalio, ali helper za number tree koji čita krivi korak pokvaren je trenutak kad bilo koja vrijednost bude brojčana, i popravljen je u istom prolazu

Popravak koraka parova u PDFlibPas number treeovima: /Nums polje je ravno niz izmjeničnih unosa ključeva i vrijednosti, pa obilazak koji testira svaki element mogao je umetnuti novi par na neparni indeks i pomaknuti kasnije parove iz takta, dok popravljeni obilazak čita ključ na X*2 i vrijednost na X*2+1
Bug se na /PageLabels rijetko opalio jer su vrijednosti oznaka rječnici, ali helper za number tree koji čita krivi korak pokvaren je čim bilo koja vrijednost bude brojčana, pa oba obilaska sada koraku u parovima

Čitanje oznaka natrag i njihov round trip

TPDFlib.GetPageLabel(Page) vraća oznaku za stranicu brojanu od 1 i ima dva fallbacka vrijedna poznavanja. Bez ikakvog /PageLabels unosa vraća decimalni broj stranice, pa ga pozivatelj može zvati bezuvjetno. S treeom prisutnim, ali bez raspona koji pokriva stranicu, vraća prazan string, što je točno ono što se događa kad datoteka preskoči obavezni unos indeksa 0; referentna dokumentacija kaže da raspon koji počinje na stranici 1 mora postojati da bi se oznake ispravno prikazivale, i kod tu zahtjev čini vidljivim. Slovni stilovi slijede specifikaciju, a ne stupce proračunske tablice: nakon Z dolazi AA, pa BB, s ponavljanjem slova umjesto prijenosa

var
  P: Integer;
  Data: WideString;
begin
  // Brzi audit onoga što će preglednik pokazati u kutiji stranice
  for P := 1 to Lib.PageCount do
    WriteLn(P, ' -> ', Lib.GetPageLabel(P));

  // Vrijednost opcije 4 izvozi samo raspone oznaka kao PageLabelBegin zapise
  Data := Lib.ExportDocumentData(4);
  // Import ih ponovno odigra kroz ClearPageLabels + AddPageLabels
  Lib.ImportDocumentData(Data, 0);
end;

Za masovne izmjene, ExportDocumentData s vrijednosti opcije 4 zapisuje svaki raspon kao blok PageLabelBegin s recima PageLabelNewIndex, PageLabelStart, PageLabelPrefix i PageLabelNumStyle, a ImportDocumentData prvi zapis oznake koji vidi tretira kao potpunu zamjenu: jednom zove ClearPageLabels, pa svaki zapis predaje metodi AddPageLabels. Time je tekstualni round trip determinističan čak i kad je izvorna datoteka koristila /Kids tree, jer brisanje skida cijeli unos kataloga, a ponovno izgrađeni tree od početka je jedan leaf

Što popravak i dalje ne jamči?

Izravnjavanje je jednosmjerno i vjeruje redoslijedu koji nađe. EnumNumTree skuplja parove u redoslijedu datoteke, a GetPageLabel primjenjuje zadnji raspon čiji je ključ manji ili jednak indeksu stranice, pa tuđa datoteka čiji su leafovi poremećenog reda — što §7.9.7 zabranjuje, ali što kruži — može i dalje davati krive oznake dok raspone ne izgradite iznova s ClearPageLabels i svježim pozivima AddPageLabels. Oznake su vezane i uz indekse stranica, a ne objekte stranica, pa svaka operacija koja mijenja broj ili redoslijed stranica ostavlja raspone gdje su bili. Zamjena u mjestu poput zamjene stranica uz očuvane brojeve objekata zadržava broj pa su time i oznake poravnate, dok spajanje poput sređivanja isprepletenih duplex skenova proizvodi novi niz stranica koji zaslužuje svježe zapisane raspone

Pozivi za oznake stranica, rukovanje number treeom te izvoz i uvoz podataka dokumenta opisani ovdje isporučuju se u PDF Library for Delphi za Delphi, C++Builder i Lazarus, s referentnim unosom za AddPageLabels koji dokumentira vrijednosti stilova i povratne kodove