Techninis straipsnis

PDF puslapių žymos Delphi: /Kids skaičių medžių taisymas

PDF Library for Delphi rašo puslapių žymų sritis su AddPageLabels, ir nuo v3.539.10 tas iškvietimas veikia ir su įkeltais failais, kurių /PageLabels skaičių medis padalintas į /Kids mazgus: šaknis prieš įleidžiant naują sritį sutraukiama į vieną /Nums lapą, tad žyma iš tiesų atsiranda peržiūros programoje, o ne tyliai ignoruojama. Tipinė auka – knyginio stiliaus PDF iš maketuoto įrankio, su romėniškais skaičiais įvade, arabiška numeracija pagrindinėje dalyje ir priedu, pažymėtu A-1, A-2, kai jūs norėjote pervadinti tik priedą, o nepasikeitė niekas

Kas yra PDF puslapių žymos ir kaip jos saugomos?

Puslapių žymos yra eilutės, kurias peržiūros programa rodo puslapių laukelyje vietoj fizinio puslapio indekso, o ISO 32000-1 §12.4.2 jas saugo kaip skaičių medį po katalogo raktu /PageLabels. Kiekvienas raktas yra 0-based puslapio indeksas, pradedantis žymų sritį, o kiekviena reikšmė – puslapių žymos žodynas su iki trijų įrašais: /S numeravimo stiliui (D, R, r, A ar a), /P priešdėlio eilutei ir /St pirmojo srities puslapio skaitinei reikšmei, kuri pagal numatymą lygi 1. Sritis tęsiasi iki kito rakto, ir specifikacija reikalauja, kad medis turėtų reikšmę puslapio indeksui 0, tad kiekvienas puslapis priklauso kokiai nors sričiai

Puslapių žymų saugojimas PDFlibPas terminais: /PageLabels skaičių medis kiekvieną sritį rakta sujaus jos 0-based starto puslapiu, kiekviena reikšmė yra žymos žodynas su /S stiliumi, /P priešdėliu ir /St pirmuoju numeriu, o knygos pavyzdys susieja romėnišką įvadą, arabiškus pagrindinės dalies puslapius ir A- priedą su trimis sritimis
Sritis galioja iki kito rakto, specifikacija reikalauja reikšmės puslapio indeksui 0, o GetPageLabel taiko paskutinę sritį, kurios raktas nėra didesnis už puslapį, tad kiekvienas puslapis išsprendžia į ką nors
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
      Exit;
    // Puslapiai 1-4: i, ii, iii, iv (mažosios romėniškos)
    Lib.AddPageLabels(1, 3, 1, '');
    // Puslapiai 5-120: 1, 2, 3 ... (dešimtainiai)
    Lib.AddPageLabels(5, 1, 1, '');
    // Puslapiai 121 ir toliau: A-1, A-2 ... (dešimtainiai su priešdėliu)
    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) savo argumentus susieja su tuo žodynu be staigmenų, kai žinote tris taisykles. Start yra 1-based, kaip ir kiekvienas kitas puslapio argumentas bibliotekoje, ir į medį įrašomas kaip Start - 1. Style kinta nuo 0 iki 5, kur 0 reiškia tik priešdėlį, o 1 iki 5 tampa /S reikšmėmis D, R, r, A ir a; kas nors už tų ribų grąžina 0 ir nieko neliečia. Offset tampa /St tik kai didesnis už nulį, tad paduodami 0 tiesiog praleidžiate raktą, ir peržiūros programa grįžta prie numatytosios 1. Kadangi puslapių žymos atsirado PDF 1.3, iškvietimas taip pat paleidžia EnsureMinVersion('1.3', '/PageLabels'), kuri pakelia senesnio failo išvesties versiją, nebent įrašymo versiją esate aiškiai užrakdinęs

Kodėl naujos puslapių žymos dingsta, kai medis turi /Kids?

Naujos žymos dingsta todėl, kad ISO 32000-1 §7.9.7 (37 lentelė) reikalauja, jog skaičių medžio šaknis neštų arba /Kids, arba /Nums – niekada abiejus, o ankstesnė NumTreeSet pagalbinė funkcija mokėjo ieškoti tik /Nums. Ilgus dokumentus gaminantys generatoriai dažnai medį padalina į tarpinius mazgus, kiekvieną su /Limits pora, ir pakabina juos ant šaknies, turinčios tik /Kids. Senasis kodas tos šaknyje /Nums nerado, sukūrė šviežią šalia egzistuojančių /Kids ir ten įleido naują sritį. Rezultatas – šaknis su dviem tarpusavyje nesuderinamais įėjimo taškais. Peržiūros programos leidžiasi pro /Kids ir niekada nežiūri į tuščiai gulinčią masyvą, pačios bibliotekos EnumNumTree taip pat pirmiausia tikrina /Kids, o NumTreeLookup atmeta mazgą, kuriame HasKids xor HasNums netiesa. AddPageLabels vis tiek grąžino 1, o įrašytas failas vis tiek atsivėrė švariai – tai blogiausia gedimo rūšis: niekas nesiskundžia, žymos tiesiog lieka tos pačios

Pataisa NumTreeSet šaknį paverčia lapu, dar nieko neįleisdama. Kai šaknis neša /Kids, EnumNumTree paeiliui apeina kiekvieną lapą ir surenka kiekvieną rakto ir reikšmės porą, iš to sąrašo sukonstruojamas naujas plokščias /Nums masyvas, o /Kids, /Limits ir bet koks pasenęs /Nums iš šaknies išvalomi, kol plokščias masyvas prikabinamas. /Limits numetimas nėra kosmetika, nes 37 lentelė tą įrašą leidžia tik tarpiniams ir lapo mazgams, niekada šakniai. Nuo tos vietos įleidimas yra paprastas surūšiuotas įdėjimas į vieną masyvą, o egzistuojančios sritys išgyvena su savo originaliais žymų žodynais. Kompromisas sąmoningas: medis po to neperstatomas į subalansuotus /Kids mazgus. Puslapių žymoms tai nieko nekainuoja, nes net didelėje atskaitos knygoje retai būna daugiau nei keliolika sričių, o vieną lapą ir taip rašo dauguma generatorių

Skaičių medžio remontas PDFlibPas: šaknis, nešanti /Kids ir tuščiai gulinčią /Nums masyvą, peržiūros programoms nematoma, nes ISO 32000-1 leidžia tik vieną iš dviejų, tad NumTreeSet sutraukia kiekvieną lapą į vieną /Nums masyvą ir išvalo /Kids bei /Limits, kurių 37 lentelė šakniai niekada neleidžia
Niekas nesiskundė, nes kiekviena patikra praėjo: AddPageLabels grąžino 1, įrašytas failas atsivėrė švariai, o naujos srities neranda tik tas skaitytuvas, kuris leidžiasi pro /Kids pirmiausia – taip, kaip daro ir peržiūros programos, ir pati biblioteka
// Pervadinkite priedą faile, kurio /PageLabels šaknis naudoja /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
  WriteLn('Before: ', Lib.GetPageLabel(121));  // pvz. A-1
  // Pakeiskite sritį, prasidedančią 121 puslapyje: App-a, App-b ...
  if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
    Lib.SaveToFile('vendor-manual-relabeled.pdf');
  // Egzistuojančios romėniškos ir dešimtainės sritys vis dar sutrauktame lape
  WriteLn('After: ', Lib.GetPageLabel(121));   // App-a
  WriteLn('Front: ', Lib.GetPageLabel(2));     // ii, nepakitę
end;

Kaip /Nums masyvas gali būti klaidingai perskaitytas kaip raktai?

/Nums masyvas perskaitomas klaidingai, kai kodas jį eina po vieną elementą, nes masyvas yra plokščia kintamų porų eiga [key0 value0 key1 value1 ...], ir tik lyginės pozicijos yra raktai. Senoji NumTreeSet kilpa tikrino kiekvieno elemento skaitinį tipą, tad reikšmė, kuri atsitiktinai buvo skaičius, būdavo lyginama tarsi būtų raktas; mažiau-nei pataikymas galėjo nustatyti įleidimo tašką ties nelyginiu indeksu ir įmesti naują porą į egzistuojančios vidurį, pasislinkęs visas vėlesnes poras iš fazės. EnumNumTree turėjo tą patį vieno žingsnio ėjimą. Dabar abi iteruoja poras dveju žingsniu, skaitodamos raktą ties X * 2 ir reikšmę ties X * 2 + 1, o tikslus rakto atitikimas pakeičia reikšmę ir išeina su Break. Sąžiningai tariant, puslapių žymų reikšmės yra žodynai, tad antrasis bugas ant paties /PageLabels suveikdavo retai, bet skaičių medžio pagalbinė funkcija, skaitanti netinkamu žingsniu, sugenda tą akimirką, kai tik viena reikšmė būna skaitinė, ir ji buvo pataisyta tą patį kartą

Porų žingsnio pataisa PDFlibPas skaičių medžiuose: /Nums masyvas yra plokščia kintamų rakto ir reikšmės įrašų eiga, tad kiekvieną elementą tikrinantis ėjimas galėjo įleisti naują porą ties nelyginiu indeksu ir paslinkti vėlesnes poras iš fazės, o pataisytas ėjimas skaito raktą ties X*2 ir reikšmę ties X*2+1
Bugas ant /PageLabels suveikdavo retai, nes žymų reikšmės yra žodynai, bet skaičių medžio pagalbinė funkcija, skaitanti netinkamu žingsniu, sugenda tą akimirką, kai tik viena reikšmė būna skaitinė, tad abi dabar žingsniuoja poromis

Žymų perskaitymas atgal ir jų perkėlimas pirmyn ir atgal

TPDFlib.GetPageLabel(Page) grąžina 1-based puslapio žymą ir turi du atsarginius elgesius, vertus žinoti. Kai /PageLabels įrašo iš viso nėra, grąžina dešimtainį puslapio numerį, tad kviečiantysis gali naudoti jį nesąlygiškai. Kai medis yra, bet nė viena sritisdengia puslapio, grąžina tuščią eilutę – būtent taip nutinka, kai failas praleidžia privalomą indekso 0 įrašą; atskaitos dokumentacija sako, kad sritis, prasidedanti 1 puslapyje, turi egzistuoti, kad žymos rodytųsi teisingai, ir kodas tą reikalavimą padaro matomą. Raidiniai stiliai seka specifikaciją, o ne skaičiuoklės stulpelius: po Z seka AA, paskui BB – raidė kartojama, o ne pernešama

var
  P: Integer;
  Data: WideString;
begin
  // Greita apžiūra, ką peržiūros programa rodys puslapių laukelyje
  for P := 1 to Lib.PageCount do
    WriteLn(P, ' -> ', Lib.GetPageLabel(P));

  // Parinkties reikšmė 4 eksportuoja tik žymų sritis kaip PageLabelBegin įrašus
  Data := Lib.ExportDocumentData(4);
  // Importas juos atkartoja pro ClearPageLabels + AddPageLabels
  Lib.ImportDocumentData(Data, 0);
end;

Masyviems redagavimams ExportDocumentData su parinkties reikšme 4 užrašo kiekvieną sritį kaip PageLabelBegin bloką su PageLabelNewIndex, PageLabelStart, PageLabelPrefix ir PageLabelNumStyle eilutėmis, o ImportDocumentData pirmąjį pamatytą žymos įrašą laiko pilnu pakeitimu: jis vienąkart iškviečia ClearPageLabels ir tada kiekvieną įrašą perduoda AddPageLabels. Dėl to tekstinė apyrankė pirmyn ir atgal yra deterministinė net tada, kai originalus failas naudojo /Kids medį, nes išvalymas pašalina visą katalogo įrašą, o atstatytas medis nuo pat pradžios yra vienas lapas

Ko pataisa vis tiek negarantuoja?

Sutraukimas yra vienakryptis ir pasitiki aptikta tvarka. EnumNumTree surenka poras failo tvarka, o GetPageLabel taiko paskutinę sritį, kurios raktas mažesnis arba lygus puslapio indeksui, tad svetimas failas, kurio lapai išdėstyti ne tvarka – ko §7.9.7 draudžia, bet kas cirkuliuoja – vis tiek gali duoti neteisingų žymų, kol nesuratysite sričių iš naujo su ClearPageLabels ir šviežiais AddPageLabels iškvietimais. Žymos taip pat susietos su puslapių indeksais, o ne puslapių objektais, tad bet kokia operacija, keičianti puslapių skaičių arba tvarką, palieka sritis ten, kur jos buvo. Vietos keitimas, toks kaip puslapių pakeitimas išlaikant objektų numerius, išlaiko skaičių, todėl žymos lieka suderintos, o suliejimas, toks kaip įpintų dvišalių skenavimų surūšiavimas, duoda naują puslapių seką, kurią verta aprašyti šviežiai įrašytomis sritimis

Čia aprašyti puslapių žymų iškvietimai, skaičių medžio apdorojimas ir dokumento duomenų eksportas bei importas atkeliauja su PDF Library for Delphi Delphi, C++Builder ir Lazarus aplinkose, o atskaitos įrašas AddPageLabels dokumentuoja stilių reikšmes ir grąžinamuosius kodus