PDF Library for Delphi kirjoittaa sivutunnistealueet AddPageLabels-kutsulla, ja versiosta v3.539.10 alkaen kutsu toimii myös ladatuilla tiedostoilla, joiden /PageLabels-numeropuu on jaettu /Kids-solmuiksi: juuri litistetään yhdeksi /Nums-lehdeksi ennen kuin uusi alue menee sisään, joten tunniste oikeasti näkyy katselimessa sen sijaan että se ohitettaisiin hiljaa. Tyypillinen uhri on taitto-ohjelman tuottama kirjatyylinen PDF, jossa on roomalaiset numerot etutekstissä, arabialainen numerointi runkotekstissä ja liite nimeltään A-1, A-2, ja jossa halusit vain nimetä liitteen uudelleen eikä mikään muuttunut
Mitä PDF:n sivutunnisteet ovat ja miten ne tallennetaan?
Sivutunnisteet ovat merkkijonoja, jotka katselin näyttää sivulaatikossaan fyysisen sivuindeksin sijaan, ja ISO 32000-1 §12.4.2 tallentaa ne numeropuuna katalogin avaimen /PageLabels alle. Jokainen avain on nollapohjainen sivuindeksi, joka aloittaa nimistysalueen, ja jokainen arvo on sivutunnistesanakirja, jossa on enintään kolme merkintää: /S numerointityylille (D, R, r, A tai a), /P etuliitemerkkijonolle ja /St alueen ensimmäisen sivun numeeriselle arvolle, joka oletuksena on 1. Alue jatkuu seuraavaan avaimeen asti, ja spesifikaatio vaatii puun sisältävän arvon sivuindeksille 0, joten jokainen sivu kuuluu johonkin alueeseen
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Sivut 1-4: i, ii, iii, iv (pienet roomalaiset numerot)
Lib.AddPageLabels(1, 3, 1, '');
// Sivut 5-120: 1, 2, 3 ... (desimaali)
Lib.AddPageLabels(5, 1, 1, '');
// Sivut 121 eteenpäin: A-1, A-2 ... (desimaali etuliitteellä)
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) kuvaa argumenttinsa kyseiseen sanakirjaan ilman yllätyksiä, kunhan tiedät kolme sääntöä. Start on ykköspohjainen kuten jokainen muukin sivuargumentti kirjastossa, ja se kirjoitetaan puuhun muodossa Start - 1. Style kulkee 0:sta 5:een, jossa 0 tarkoittaa vain etuliitettä ja 1–5 muuttuvat /S-arvoiksi D, R, r, A ja a; mikä tahansa alueen ulkopuolinen palauttaa 0:n eikä koske mihinkään. Offset muuttuu /St:ksi vain, kun se on suurempi kuin nolla, joten 0:n välittäminen jättää avaimen väliin ja katselin palaa oletukseen 1. Koska sivutunnisteet saapuivat PDF 1.3:een, kutsu ajaa myös EnsureMinVersion('1.3', '/PageLabels')-kutsun, joka nostaa vanhemman tiedoston ulostuloversion, ellet ole nimenomaisesti lukinnut tallennusversiota
Miksi uudet sivutunnisteet katoavat, kun puussa on /Kids?
Uudet tunnisteet katoavat, koska ISO 32000-1 §7.9.7 (taulukko 37) määrää numeropuun juuren kantamaan joko /Kids- tai /Nums-merkinnän, ei koskaan molempia, ja aiempi NumTreeSet-apuri osasi etsiä vain /Nums-merkintää. Pitkiä asiakirjoja tuottavat tuottajat jakavat usein puun välisolmuihin, joilla kullakin on /Limits-pari, ja ripustavat ne juureen, jossa on vain /Kids. Vanha koodi ei löytänyt /Nums-merkintää kyseisestä juuresta, loi tuoreen viereen olemassa olevan /Kidsin ja työnsi uuden alueen sinne. Tuloksena oli juuri, jolla on kaksi toisensa poissulkevaa sisääntuloa. Katselimet laskeutuvat /Kidsin kautta eivätkä koskaan katso harhailevaa taulukkoa, kirjaston oma EnumNumTreekin tarkistaa /Kidsin ensin, ja NumTreeLookup kieltäytyy solmusta, jossa HasKids xor HasNums on epätosi. AddPageLabels palautti silti 1:n ja tallennettu tiedosto avautui silti siististi, mikä on pahin mahdollinen vian laji: mikään ei valita, tunnisteet vain pysyvät samanlaisina
Korjaus NumTreeSetissä muuttaa juuren lehdeksi ennen mitään lisäystä. Kun juuri kantaa /Kids-merkintää, EnumNumTree kävelee jokaisen lehden järjestyksessä ja kerää jokaisen avain-arvo-parin, tuore litteä /Nums-taulukko rakennetaan kyseisestä listasta, ja /Kids, /Limits ja mahdollinen vanhentunut /Nums puhdistetaan juuresta ennen kuin litteä taulukko liitetään. /Limitsin pudottaminen ei ole kosmetiikkaa, sillä taulukko 37 sallii kyseisen merkinnän vain välisolmuilla ja lehdillä, ei koskaan juuressa. Siitä eteenpäin lisäys on tavallinen järjestetty lisäys yhteen taulukkoon, ja olemassa olevat alueet selviävät alkuperäisine tunnistesanakirjoineen. Kompromissi on tahallinen: puuta ei rakenneta uudelleen tasapainoisiksi /Kids-solmuiksi jälkikäteen. Sivutunnisteille se ei maksa mitään, sillä edes suurella hakuteoksella on harvoin enempää kuin pari kymmentä aluetta, ja yksittäinen lehti on se, mitä useimmat tuottajat kirjoittavat joka tapauksessa
// Nimetään liite uudelleen tiedostossa, jonka /PageLabels-juuri käyttää /Kids-merkintää
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // esim. A-1
// Korvataan alue, joka alkaa sivulta 121: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Olemassa olevat roomalaiset ja desimaalialueet ovat yhä litistetyssä lehdessä
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, muuttumaton
end;
Miten /Nums-taulukko voidaan lukea väärin avaimina?
/Nums-taulukko tulkitaan väärin, kun koodi kävelee sitä yksi alkio kerrallaan, sillä taulukko on litteä jakso vuorottelevia pareja, [key0 value0 key1 value1 ...], ja vain parilliset paikat ovat avaimia. Vanha NumTreeSet-silmukka testasi jokaista alkiota numeeriselta tyypiltä, joten arvo, joka sattui olemaan numero, verrattiin niin kuin se olisi ollut avain; pienempi kuin -osuma saattoi asettaa lisäyskohdan parittomaan indeksiin ja pudottaa uuden parin olemassa olevan keskelle, siirtäen jokaisen myöhemmän parin pois tahdistaan. EnumNumTreellä oli sama yhden askeleen kävely. Molemmat iteroivat nyt pareja askelluksella kaksi, lukevat avaimen kohdasta X * 2 ja arvon kohdasta X * 2 + 1, ja täsmällinen avainosuma korvaa arvon ja poistuu Breakilla. Reiluuttaan: sivutunnisteiden arvot ovat sanakirjoja, joten tämä toinen bugi lauhtui harvoin /PageLabelsissa itsessään, mutta numeropuu-apuri, joka lukee väärällä askelluksella, on turmeltunut sillä hetkellä kun mikä tahansa arvo on numeerinen, ja se korjattiin samalla reissulla
Tunnisteiden lukeminen takaisin ja edestakainen kuljetus
TPDFlib.GetPageLabel(Page) palauttaa tunnisteen ykköspohjaiselle sivulle, ja sillä on kaksi varajärjestelyä, jotka kannattaa tuntea. Ilman mitään /PageLabels-merkintää se palauttaa desimaalisen sivunumeron, joten kutsuja voi käyttää sitä ehdottomasti. Kun puu on olemassa mutta mikään alue ei kata sivua, se palauttaa tyhjän merkkijonon, mikä on täsmälleen se, mitä tapahtuu, kun tiedosto ohittaa pakollisen indeksin 0 merkinnän; referenssidokumentaatio sanoo, että sivulta 1 alkavan alueen on oltava olemassa, jotta tunnisteet näkyvät oikein, ja koodi tekee tuon vaatimuksen näkyväksi. Kirjaintyylit noudattavat spesifikaatiota eivätkä taulukkolaskennan sarakkeita: Z:n jälkeen tulee AA, sitten BB, toistaen kirjainta sen sijaan että kantaisiin yli
var
P: Integer;
Data: WideString;
begin
// Nopea tarkastus siitä, mitä katselin näyttää sivulaatikossaan
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Optioarvo 4 vie vain tunnistealueet PageLabelBegin-tietueina
Data := Lib.ExportDocumentData(4);
// Tuonti toistaa ne ClearPageLabels + AddPageLabels -kutsujen kautta
Lib.ImportDocumentData(Data, 0);
end;
Massamuokkauksissa ExportDocumentData optioarvolla 4 kirjoittaa jokaisen alueen PageLabelBegin-lohkona, jossa on PageLabelNewIndex-, PageLabelStart-, PageLabelPrefix- ja PageLabelNumStyle-rivit, ja ImportDocumentData katsoo ensimmäisen näkemänsä tunnistetietueen täydelliseksi korvaukseksi: se kutsuu ClearPageLabelsia kerran ja syöttää sitten jokaisen tietueen AddPageLabelsille. Se tekee tekstimuotoisesta edestakaisesta siirrosta deterministisen silloinkin, kun alkuperäinen tiedosto käytti /Kids-puuta, koska tyhjennys poistaa koko katalogimerkinnän ja rakennettu puu on alusta alkaen yksi lehti
Mitä korjaus ei silti takaa?
Litistäminen on yksisuuntaista ja luottaa löytämäänsä järjestykseen. EnumNumTree kerää parit tiedostojärjestyksessä, ja GetPageLabel soveltaa viimeistä aluetta, jonka avain on pienempi tai yhtä suuri kuin sivuindeksi, joten vieras tiedosto, jonka lehdet ovat epäjärjestyksessä — mitä §7.9.7 kieltää mutta joka kiertää silti — voi tuottaa yhä vääriä tunnisteita, kunnes rakennat alueet uudelleen ClearPageLabelsilla ja tuoreilla AddPageLabels-kutsuilla. Tunnisteet on sidottu myös sivuindekseihin, eivät sivuobjekteihin, joten mikä tahansa operaatio, joka muuttaa sivumäärää tai järjestystä, jättää alueet sinne minne ne jäivät. Paikallaan tehtävä vaihto, kuten sivujen korvaaminen objektinumeroita säilyttäen, pitää määrän ja siten tunnisteet linjassa, kun taas yhdistäminen, kuten lomittain vuorottelevien kaksipuolisten skannausten yhdistely, tuottaa uuden sivusekvenssin, joka ansaitsee tuoreen aluejoukon
Tässä kuvatut sivutunnistekutsut, numeropuun käsittely sekä asiakirjadatan vienti ja tuonti toimitetaan kaikkineen PDF Library for Delphissä Delphille, C++Builderille ja Lazarukseille, ja AddPageLabelsin referenssimerkintä dokumentoi tyyliluvut ja paluukoodit