PDF Library for Delphi zapisuje razpone oznak strani z AddPageLabels, od v3.539.10 pa ta klic deluje tudi na naloženih datotekah, katerih številčno drevo /PageLabels je razdeljeno na vozlišča /Kids: koren se pred vnosom novega razpona zravna v en sam list /Nums, zato oznaka dejansko pride na vidik v pregledovalniku, namesto da bi bila tiho prezrta. Tipična žrtev je PDF v knjižni obliki iz orodja za uložke, z rimskimi številkami v uvodnem delu, arabskim štetjem v glavnem telesu in prilogo, označeno A-1, A-2, kjer ste želeli preoznačiti samo prilogo in se ni spremenilo nič
Kaj so oznake strani PDF in kako so shranjene?
Oznake strani so nizi, ki jih pregledovalnik pokaže v svoji škatli strani namesto fizičnega kazala strani, ISO 32000-1 §12.4.2 pa jih shrani kot številčno drevo pod ključem kataloga /PageLabels. Vsak ključ je kazalo strani od 0, ki začne razpon označevanja, vsaka vrednost pa je slovar oznake strani z do tremi vnosi: /S za slog štetja (D, R, r, A ali a), /P za niz predpone in /St za številčno vrednost prve strani v razponu, ki privzame 1. Razpon teče do naslednjega ključa, specifikacija pa zahteva, da drevo vsebuje vrednost za kazalo strani 0, tako da vsako stran pokrije kak razpon
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Strani 1-4: i, ii, iii, iv (male rimske)
Lib.AddPageLabels(1, 3, 1, '');
// Strani 5-120: 1, 2, 3 ... (decimalno)
Lib.AddPageLabels(5, 1, 1, '');
// Strani 121 dalje: A-1, A-2 ... (decimalno s predpono)
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) preslika svoje argumente na ta slovar brez presenečenj, ko poznate tri pravila. Start šteje od 1 kot vsak drug argument strani v knjižnici in se v drevo zapiše kot Start - 1. Style teče od 0 do 5, kjer 0 pomeni samo predpono, 1 do 5 pa postanejo vrednosti /S D, R, r, A in a; kar koli zunaj tega razpona vrne 0 in se ne dotakne ničesar. Offset postane /St le, kadar je večji od nič, zato podati 0 preprosto izpusti ključ in pregledovalnik pade nazaj na privzeto 1. Ker so oznake strani prišle v PDF 1.3, klic požene še EnsureMinVersion('1.3', '/PageLabels'), ki dvigne izhodno različico starejše datoteke, razen če ste različico shranjevanja izrecno zaklenili
Zakaj nove oznake strani izginejo, ko ima drevo /Kids?
Nove oznake izginejo, ker ISO 32000-1 §7.9.7 (tabela 37) določa, da koren številčnega drevesa nosi ali /Kids ali /Nums, nikoli oboje, starejši pomožnik NumTreeSet pa je znal iskati le /Nums. Proizvajalci dolgih dokumentov drevo pogosto razdelijo na vmesna vozlišča, vsako s parom /Limits, in obesijo na koren, ki ima le /Kids. Stara koda na tem korenu ni našla /Nums, ustvarila je svežega poleg obstoječega /Kids in vanj vstavila nov razpon. Rezultat je bil koren z dvema medsebojno izključujočima vstopoma. Pregledovalniki spustijo skozi /Kids in stranskega polja nikoli ne pogledajo, knjižničin lasten EnumNumTree prav tako najprej preveri /Kids, NumTreeLookup pa zavrne vozlišče, kjer je HasKids xor HasNums neresnično. AddPageLabels je še vedno vrnil 1 in shranjena datoteka se je še vedno čisto odprla, kar je najslabša vrsta odpovedi: nič se ne pritoži, oznake pa ostanejo iste
Popravek v NumTreeSet pretvori koren v list, preden karkoli vstavi. Ko koren nosi /Kids, EnumNumTree sprehodi vsak list po vrsti in pobere vsak par ključ in vrednost, iz tega seznama se zgradi novo ravno polje /Nums, /Kids, /Limits in morebiten zastarel /Nums pa se s korena počistijo, preden se ravno polje pripne. Odstranitev /Limits ni kozmetika, saj tabela 37 ta vnos dovoljuje le na vmesnih in listnih vozliščih, nikoli na korenu. Od takrat naprej je vstavljanje navaden urejen vnos v eno polje, obstoječi razponi pa preživijo s svojimi izvirnimi slovarji oznak. Kompromis je nameren: drevo se nato ne zgradi znova v uravnotežena vozlišča /Kids. Za oznake strani to ne stane nič, ker tudi velik priročnik redko ima več kot nekaj ducatov razponov, in en sam list je tisto, kar večina proizvajalcev vseeno zapiše
// Preoznačitev priloge v datoteki, katere koren /PageLabels uporablja /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // npr. A-1
// Zamenjava razpona, ki se začne na strani 121: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Obstoječi rimski in decimalni razponi so še vedno v zravnanem listu
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, nespremenjeno
end;
Kako je lahko polje /Nums zmotno prebrano kot ključi?
Polje /Nums se prebere narobe, ko koda stopa po njem en element naenkrat, ker je polje raven tek izmeničnih parov, [ključ0 vrednost0 ključ1 vrednost1 ...], ključi pa so le soda mesta. Stara zanka NumTreeSet je vsak element preizkusila za številski tip, zato se je vrednost, ki je slučajno bila številka, primerjala, kot da bi bila ključ; zadetek manjši-od je lahko določil vstavljeno točko na liho mesto in spustil nov par na sredino obstoječega, s čimer je vsak poznejši par zamaknil iz faze. EnumNumTree je imel isti enokorakni sprehod. Oba zdaj ponavljata pare s korakom dva, berejo ključ na X * 2 in vrednost na X * 2 + 1, točno ujemanje ključa pa zamenja vrednost in izstopi z Break. Pošteno povedano, vrednosti oznak strani so slovarji, zato se ta druga napaka na /PageLabels samem redko sproži, pomožnik številčnega drevesa, ki bere napačen korak, pa je pokvarjen v trenutku, ko je katera koli vrednost številska, in popravljen je bil v istem skledu
Branje oznak nazaj in njihov povratni prehod
TPDFlib.GetPageLabel(Page) vrne oznako za stran, štešto od 1, in ima dva rezervna načina, vredna vedenja. Brez vnosa /PageLabels sploh vrne decimalno številko strani, zato ga klicatelj lahko uporabi brezpogojno. Z drevesom, ki je prisotno, a brez razpona, ki pokriva stran, vrne prazen niz, točno to pa se zgodi, ko datoteka izpusti obvezen vnos kazala 0; referenčna dokumentacija pravi, da mora za pravilen prikaz oznak obstajati razpon, ki se začne na strani 1, koda pa to zahtevo naredi vidno. Črkovni slogi sledijo specifikaciji in ne stolpcem preglednic: po Z pride AA, nato BB, ki ponovi črko namesto da bi prenašal
var
P: Integer;
Data: WideString;
begin
// Hitri pregled, kaj bo pregledovalnik pokazal v svoji škatli strani
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Vrednost možnosti 4 izvozi le razpone oznak kot zapise PageLabelBegin
Data := Lib.ExportDocumentData(4);
// Uvoz jih ponovno predvaja prek ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Za množična urejanja ExportDocumentData z vrednostjo možnosti 4 zapiše vsak razpon kot blok PageLabelBegin z vrsticami PageLabelNewIndex, PageLabelStart, PageLabelPrefix in PageLabelNumStyle, ImportDocumentData pa prvi zapis oznake, ki ga vidi, obravnava kot popolno zamenjavo: enkrat pokliče ClearPageLabels in nato vsak zapis dovaja AddPageLabels. To naredi besedilni povratni prehod determinističen, tudi če je izvirna datoteka uporabila drevo /Kids, ker čiščenje odstrani celoten vnos kataloga in zgrajeno drevo je od začetka en sam list
Česa popravek še vedno ne zagotavlja?
Zravnanje je enosmerno in zaupa vrstnemu redu, ki ga najde. EnumNumTree pobere pare v vrstnem redu datoteke, GetPageLabel pa uporabi zadnji razpon, katerega ključ je manjši ali enak kazalu strani, zato lahko tuja datoteka, katere listi so izven vrstnega reda, kar §7.9.7 prepoveduje, a kroži po svetu, še vedno da napačne oznake, dokler razponov ne zgradite znova s ClearPageLabels in svežimi klici AddPageLabels. Oznake so vezane tudi na kazala strani in ne na objekte strani, zato vsaka operacija, ki spremeni število ali vrstni red strani, pusti razpone tam, kjer so bili. Zamenjava na mestu, kot je zamenjava strani z ohranjanjem številk objektov, ohrani število in s tem oznake poravnane, združevanje, kot je zlaganje prepletenih dvostranskih skenov, pa da novo zaporedje strani, ki zasluži sveže zapisan nabor razponov
Klici oznak strani, ravnanje s številčnim drevesom ter izvoz in uvoz podatkov dokumenta, opisani tukaj, vsi prihajajo v PDF Library for Delphi za Delphi, C++Builder in Lazarus, referenčni vnos za AddPageLabels pa dokumentira vrednosti slogov in vračilne kode