PDF Library for Delphi upisuje page label rangove kroz AddPageLabels, i od v3.539.10 taj poziv radi i na učitanim fajlovima čiji je /PageLabels number tree podeljen na /Kids čvorove: koren se izravna u jedan /Nums list pre nego što novi rang uđe, pa se labela zaista pojavi u pregledaču umesto da bude tiho ignorisana. Tipična žrtva je PDF nalik na knjigu iz layout alata, sa rimskim brojevima u prednjem delu, arapskim numerisanjem u glavi i prilogom označenim A-1, A-2, gde ste hteli samo da prelabelite prilog a ništa se nije promenilo
Šta su PDF page labele i kako se čuvaju?
Page labele su stringovi koje pregledač pokazuje u svom polju stranice umesto fizičkog indeksa stranice, a ISO 32000-1 §12.4.2 ih čuva kao number tree pod ključem kataloga /PageLabels. Svaki ključ je indeks stranice sa bazom nula koji započinje labeling rang, a svaka vrednost je page label rečnik sa najviše tri unosa: /S za stil numerisanja (D, R, r, A ili a), /P za prefiks string, i /St za numeričku vrednost prve stranice u rangu, koja po podrazumevanju glasi 1. Rang traje do sledećeg ključa, i specifikacija zahteva da drvo sadrži vrednost za indeks stranice 0, pa je svaka stranica pokrivena nekim rangom
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 ... (decimalno)
Lib.AddPageLabels(5, 1, 1, '');
// Stranice 121 i dalje: A-1, A-2 ... (decimalno sa 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) mapira svoje argumente na taj rečnik bez iznenađenja čim znate tri pravila. Start je sa bazom 1 kao i svaki drugi page argument u biblioteci, i u drvo se upisuje kao Start - 1. Style ide od 0 do 5, gde 0 znači samo prefiks a 1 do 5 postaju /S vrednosti D, R, r, A i a; sve van toga vraća 0 i ništa ne dira. Offset postaje /St samo kad je veći od nule, pa prosleđivanje 0 jednostavno izostavlja ključ i pregledač se vraća na podrazumevanu 1. Pošto su page labele stigle sa PDF 1.3, poziv pokreće i EnsureMinVersion('1.3', '/PageLabels'), koji podigne izlaznu verziju starijeg fajla osim ako ste verziju čuvanja eksplicitno zaključali
Zašto nove page labele nestaju kad drvo ima /Kids?
Nove labele nestaju jer ISO 32000-1 §7.9.7 (Tabela 37) zahteva da koren number tree-a nosi ili /Kids ili /Nums, nikad oba, a stariji NumTreeSet helper znao je samo da traži /Nums. Proizvođači dugih dokumenata često dele drvo na međučvorove, svaki sa parom /Limits, i okače ih na koren koji ima samo /Kids. Stari kod nije našao /Nums na tom korenu, napravio je svež pored postojećih /Kids, i ubacio novi rang tamo. Rezultat je bio koren sa dva međusobno isključiva ulaza. Pregledači silaze kroz /Kids i nikad ne pogledaju odmetnuti niz, i bibliotečki EnumNumTree takođe prvo proverava /Kids, a NumTreeLookup odbija čvor gde HasKids xor HasNums nije tačno. AddPageLabels je i dalje vratio 1 i sačuvani fajl se i dalje uredno otvorio, što je najgora vrsta kvara: ništa ne prigovara, labele samo ostanu iste
Popravka u NumTreeSet-u pretvara koren u list pre nego što bilo šta ubaci. Kada koren nosi /Kids, EnumNumTree obilazi svaki list redom i skuplja svaki par ključ-vrednost, novi ravan /Nums niz se gradi iz te liste, a /Kids, /Limits i svaki zastareli /Nums se očiste sa korena pre nego što se ravan niz zakači. Bacanje /Limits nije kozmetika, jer Tabela 37 dozvoljava taj unos samo na međučvorovima i listovima, nikad na korenu. Od tog trenutka umetanje je običan sortirani umetak u jedan niz, a postojeći rangovi prežive sa svojim originalnim label rečnicima. Kompromis je namera: drvo se posle ne gradi ponovo u izbalansirane /Kids čvorove. Za page labele to ne košta ništa, jer čak i veliki priručnik retko ima više od nekoliko desetina rangova, a jedan list je ionako ono što većina proizvođača upiše
// Prelabelite prilog u fajlu čiji /PageLabels koren koristi /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // npr. A-1
// Zamenite rang 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 rangovi i dalje su u izravnanom listu
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, nepromenjeno
end;
Kako /Nums niz može da se pogrešno pročita kao ključevi?
/Nums niz se pogrešno pročita kad ga kod obilazi jedan element po jedan, jer je niz ravna traka naizmeničnih parova, [key0 value0 key1 value1 ...], i samo parne pozicije su ključevi. Stara NumTreeSet petlja je testirala svaki element za numerički tip, pa je vrednost koja je slučajno bila broj poređena kao da je ključ; pogodak „manje od” mogao je da postavi tačku umetanja na neparan indeks i spusti novi par usred postojećeg, pomerajući svaki kasniji par iz takta. EnumNumTree je imao isti obilazak korak po korak. Obe sada iteriraju parove sa korakom od dva, čitajući ključ na X * 2 i vrednost na X * 2 + 1, i tačno poklapanje ključa zamenjuje vrednost i izlazi sa Break. Da budemo fer, page label vrednosti su rečnici, pa ovaj drugi bug retko puca na samom /PageLabels-u, ali number-tree helper koji čita pogrešan korak je pokvaren u trenutku kad je bilo koja vrednost broj, i popravljen je u istom prolazu
Čitanje labela nazad i njihov round trip
TPDFlib.GetPageLabel(Page) vraća labelu za stranicu sa bazom 1 i ima dva rezervna ishoda koja vredi znati. Bez ikakvog /PageLabels unosa vraća decimalni broj stranice, pa ga pozivalac može koristiti bezuslovno. Sa drvetom prisutnim ali bez ranga koji pokriva stranicu vraća prazan string, što je tačno ono što se dešava kad fajl preskoči obavezni unos indeksa 0; referentna dokumentacija kaže da rang koji počinje na stranici 1 mora postojati da bi se labele ispravno prikazivale, i kod tu zahtev čini vidljivim. Slovni stilovi prate specifikaciju, a ne kolone proračunskih tablica: posle Z dolazi AA, pa BB, ponavljajući slovo umesto prenošenja
var
P: Integer;
Data: WideString;
begin
// Brzi pregled onoga što će pregledač pokazati u svom polju stranice
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Vrednost opcije 4 izvozi samo label rangeove kao PageLabelBegin zapise
Data := Lib.ExportDocumentData(4);
// Uvoz ih ponovo pokreće kroz ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Za masovne izmene, ExportDocumentData sa vrednošću opcije 4 upisuje svaki rang kao PageLabelBegin blok sa linijama PageLabelNewIndex, PageLabelStart, PageLabelPrefix i PageLabelNumStyle, a ImportDocumentData prvi label zapis koji vidi tretira kao potpunu zamenu: jednom pozove ClearPageLabels pa svaki zapis predaje AddPageLabels-u. To tekstualni round trip čini determinističnim čak i kad je originalni fajl koristio /Kids drvo, jer brisanje skida ceo katalog unos, a ponovo izgrađeno drvo je od početka jedan list
Šta popravka i dalje ne garantuje?
Izravnavanje je jednosmerno i veruje redosledu koji nađe. EnumNumTree skuplja parove po redosledu u fajlu, a GetPageLabel primenjuje poslednji rang čiji je ključ manji ili jednak indeksu stranice, pa tuđi fajl čiji su listovi van reda, što §7.9.7 zabranjuje ali što cirkuliše, i dalje može dati pogrešne labele dok rangove ne izgradite ponovo sa ClearPageLabels i svežim AddPageLabels pozivima. Labele su takođe vezane za indekse stranica, a ne za objekte stranica, pa svaka operacija koja menja broj ili redosled stranica ostavlja rangove tamo gde su bili. Zamena na mestu poput zamene stranica uz očuvanje brojeva objekata zadržava broj pa su zato i labele poravnate, dok spajanje poput sređivanja isprepletenih dupleks skenova proizvodi novi redosled stranica koji zaslužuje sveže upisane rangove
Pozivi za page labele, rukovanje number tree-om i izvoz i uvoz podataka dokumenta opisani ovde isporučuju se u PDF Library for Delphi za Delphi, C++Builder i Lazarus, sa referentnim unosom za AddPageLabels koji dokumentuje stil vrednosti i povratne kodove