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
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
// 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
Č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