Tekninen artikkeli

PDF:n sivutunnisteet Delphissä: /Kids-numeropuun korjaus

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

Sivutunnisteiden tallennus PDFlibPasin käsittein: /PageLabels-numeropuu avainnee jokaisen alueen sen nollapohjaisella aloitussivulla, jokainen arvo on tunnistesanakirja, jossa on /S-tyyli, /P-etuliite ja /St-ensimmäinen numero, ja kirjaesimerkki kuvaa roomalaisen etutekstin, arabialaiset runkosivut ja A-liitteen kolmeksi alueeksi
Alue jatkuu seuraavaan avaimeen asti, spesifikaatio vaatii arvon sivuindeksille 0, ja GetPageLabel soveltaa viimeistä aluetta, jonka avain on sivun kohdalla tai sen alla, joten jokainen sivu ratkeaa joksikin
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

Numeropuun korjaus PDFlibPasissa: juuri, joka kantaa /Kids-merkintää ja harhailevaa /Nums-taulukkoa, on näkymätön katselimille, koska ISO 32000-1 sallii vain toisen kahdesta, joten NumTreeSet litistää jokaisen lehden yhdeksi /Nums-taulukoksi ja puhdistaa /Kids- ja /Limits-merkinnät, joita taulukko 37 ei koskaan salli juuressa
Mikään ei valittanut, koska jokainen tarkistus meni läpi: AddPageLabels palautti 1:n, tallennettu tiedosto avautui siististi, ja vain lukija, joka laskeutuu /Kidsin kautta ensin niin kuin katselimet ja kirjasto itse molemmat tekevät, ei koskaan löydä uutta aluetta
// 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

Parin askelluksen korjaus PDFlibPasin numeropuissa: /Nums-taulukko on litteä jakso vuorottelevia avain- ja arvomerkintöjä, joten jokaista alkiota testaava kävely saattoi lisätä uuden parin parittomaan indeksiin ja siirtää myöhemmät parit pois tahdistaan, kun taas korjattu kävely lukee avaimen kohdasta X*2 ja arvon kohdasta X*2+1
Bugi lauhtui harvoin /PageLabelsissa, koska tunnisteiden arvot ovat sanakirjoja, mutta väärällä askelluksella lukeva numeropuu-apuri turmeltuu sillä hetkellä kun mikä tahansa arvo on numeerinen, joten molemmat kävelyt astuvat nyt pareittain

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