Műszaki cikk

PDF oldalcímkék Delphiben: a /Kids számfa javítása

A PDF Library for Delphi az AddPageLabels-szal írja az oldalcímke tartományokat, és a v3.539.10 óta ez a hívás olyan betöltött fájlokon is működik, amiknek a /PageLabels számfa /Kids csomópontokra van bontva: a gyökér egyetlen /Nums lappá lapul ki, mielőtt az új tartomány bekerülne, így a címke tényleg megjelenik a megjelenítőben ahelyett, hogy csendben figyelmen kívül hagynák. A tipikus áldozat egy tördelőeszközből származó könyv-stílusú PDF, római számokkal az eleji részeken, arab számozással a szövegtörzsben és egy A-1, A-2 jelzésű függelékkel, ahol csak a függeléket akartad újracímkézni, és semmi sem változott

Mik azok a PDF oldalcímkék, és hogyan tárolódnak?

Az oldalcímkék azok a stringek, amiket a megjelenítő az oldalablakában mutat a fizikális oldalindex helyett, az ISO 32000-1 §12.4.2-e pedig számfaként tárolja őket a katalógus /PageLabels kulcsa alatt. Minden kulcs egy nullától induló oldalindex, ami egy címkézési tartományt nyit, minden érték pedig egy oldalcímke szótár legfeljebb három bejegyzéssel: /S a számozási stílushoz (D, R, r, A vagy a), /P egy prefix stringhez, /St pedig a tartomány első oldalának numerikus értékéhez, ami alapértelmezetten 1. Egy tartomány a következő kulcsig tart, a specifikáció pedig megköveteli, hogy a fa tartalmazzon értéket a 0. oldalindexhez, így minden oldalt lefed valamelyik tartomány

Oldalcímke-tárolás PDFlibPas szóhasználatban: a /PageLabels számfa minden tartományt a nullától induló kezdőoldala szerint kulcsol, minden érték egy címkeszótár /S stílussal, /P prefixszel és /St kezdőszámmal, a könyvpélda pedig a római eleji részeket, az arab szövegtörzs oldalait és egy A- függeléket három tartományra képez le
Egy tartomány a következő kulcsig tart, a specifikáció megkövetel egy értéket a 0. oldalindexhez, és a GetPageLabel azt az utolsó tartományt alkalmazza, aminek a kulcsa az oldalon vagy az alatt van, így minden oldal valamire feloldódik
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
      Exit;
    // 1-4. oldalak: i, ii, iii, iv (kisbetűs római)
    Lib.AddPageLabels(1, 3, 1, '');
    // 5-120. oldalak: 1, 2, 3 ... (decimális)
    Lib.AddPageLabels(5, 1, 1, '');
    // 121-től: A-1, A-2 ... (decimális prefixszel)
    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;

A TPDFlib.AddPageLabels(Start, Style, Offset, Prefix) akkor képezi le az argumentumait arra a szótárra meglepetések nélkül, ha ismered a három szabályt. A Start 1-től indul, akárcsak a library minden más oldalargumentuma, és Start - 1-ként íródik a fába. A Style 0-tól 5-ig megy, ahol a 0 csak prefixet jelent, az 1-től 5-ig pedig /S értékként D, R, r, A és a lesz; bármi, ami ezen a tartományon kívül esik, 0-t ad vissza és semmit nem érint. Az Offset csak akkor válik /St-vé, ha nagyobb nullánál, így a 0 átadása egyszerűen elhagyja a kulcsot, és a megjelenítő az 1-es alapértékre esik vissza. Mivel az oldalcímkék a PDF 1.3-ban érkeztek, a hívás lefuttatja az EnsureMinVersion('1.3', '/PageLabels')-t is, ami egy régebbi fájl kimeneti verzióját megemeli, hacsak nem zároltad expliciten a mentési verziót

Miért tűnnek el az új oldalcímkék, amikor a fa /Kids-t hordoz?

Az új címkék azért tűnnek el, mert az ISO 32000-1 §7.9.7-e (37. táblázat) úgy szabja meg a számfa gyökerét, hogy az vagy /Kids-t vagy /Nums-t hordozzon, soha mindkettőt, a korábbi NumTreeSet helper pedig csak azt tudta, hogyan keressen /Nums-t. A hosszú dokumentumokat gyártó producertek gyakran köztes csomópontokra bontják a fát, mindegyiket egy /Limits párral, és egy csak /Kids-t hordozó gyökérre akasztják. A régi kód nem talált /Nums-t azon a gyökéren, frisset gyártott a meglévő /Kids mellé, és oda szúrta be az új tartományt. Az eredmény egy gyökér lett két egymást kizáró belépési ponttal. A megjelenítők a /Kids-en keresztül ereszkednek, és soha nem nézik meg a kóbor tömböt, a library saját EnumNumTree-e is először a /Kids-t ellenőrzi, a NumTreeLookup pedig elutasít egy csomópontot, ahol a HasKids xor HasNums hamis. A AddPageLabels továbbra is 1-et adott vissza, és a mentett fájl továbbra is tisztán nyílt meg, ami a legrosszabb fajta hiba: semmi nem panaszkodik, a címkék egyszerűen ugyanazok maradnak

A NumTreeSet-beli javítás a gyökeret lappá alakítja, mielőtt bármit beszúrna. Amikor a gyökér /Kids-t hordoz, az EnumNumTree sorban bejárja minden levelét, és összegyűjti minden kulcs-érték párt, ebből a listából új, lapos /Nums tömb épül, a /Kids, a /Limits és minden elavult /Nums pedig kitisztul a gyökérből, mielőtt a lapos tömb rákerül. A /Limits elhagyása nem kozmetika, mert a 37. táblázat szerint ez a bejegyzés csak köztes és levél csomópontokon megengedett, a gyökéren soha. Attól a ponttól a beszúrás közönséges rendezett beszúrás egyetlen tömbbe, és a meglévő tartományok túlélik az eredeti címkeszótáraikkal. A kompromisszum szándékos: a fát nem építik újra kiegyensúlyozott /Kids csomópontokká utána. Oldalcímkékre ez semmit sem kerül, mert még egy nagy referenciakézikönyvnek is ritkán van több mint néhány tucat tartománya, és a legtöbb producer amúgy is egyetlen levelet ír

Számfa-javítás a PDFlibPas-ben: egy /Kids-et és kóbor /Nums tömböt hordozó gyökér láthatatlan a megjelenítőknek, mert az ISO 32000-1 csak a kettő egyikét engedi, ezért a NumTreeSet minden levelet egyetlen /Nums tömbbé lapít ki, és kisöpri a /Kids-et és a /Limits-et, amit a 37. táblázat soha nem enged a gyökéren
Semmi nem panaszkodott, mert minden ellenőrzés átment: az AddPageLabels 1-et adott vissza, a mentett fájl tisztán nyílt meg, és csak egy olyan olvasó nem találja meg az új tartományt, amelyik először a /Kids-en ereszkedik le, ahogy a megjelenítők és maga a library is teszik
// A függelék újracímkézése olyan fájlban, aminek a /PageLabels gyökere /Kids-t használ
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
  WriteLn('Before: ', Lib.GetPageLabel(121));  // e.g. A-1
  // A 121. oldalon kezdődő tartomány cseréje: App-a, App-b ...
  if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
    Lib.SaveToFile('vendor-manual-relabeled.pdf');
  // A meglévő római és decimális tartományok még a kilapított levélben vannak
  WriteLn('After: ', Lib.GetPageLabel(121));   // App-a
  WriteLn('Front: ', Lib.GetPageLabel(2));     // ii, változatlan
end;

Hogyan olvasható félre kulcsokként egy /Nums tömb?

Egy /Nums tömb akkor olvasódik félre, amikor a kód elemenként járja be, mert a tömb váltakozó párok lapos futása, [key0 value0 key1 value1 ...], és csak a páros pozíciók kulcsok. A régi NumTreeSet ciklus minden elemet numerikus típusra tesztelt, így egy véletlenül számot adó érték kulcsként hasonlítódott; egy kisebb-egyelő találat a beszúrási pontot páratlan indexre tehette, és az új párt egy meglévő közepére ejthette, eltolva minden későbbi párt fázison kívülre. Az EnumNumTree ugyanazt az egylépéses bejárást hordozta. Mindkettő most kettes lépésközzel iterálja a párokat, a kulcsot X * 2-nél, az értéket X * 2 + 1-nél olvasva, és egy pontos kulcsegyezés lecseréli az értéket, majd Break-kel lép ki. Az igazsághoz hozzátartozik, hogy az oldalcímkék értékei szótárak, így ez a második hiba ritkán aktiválódott magán a /PageLabels-en, de egy számfa-helper, ami rossz lépésközzel olvas, abban a pillanatban romlásba megy, amint bármelyik érték szám, és ugyanabban a menetben javították

Párok lépésköz-javítása a PDFlibPas számfáiban: egy /Nums tömb váltakozó kulcs- és értékbejegyzések lapos futása, így egy minden elemet tesztelő bejárás páratlan indexre szúrhatott be új párt, és későbbi párokat tolhatott fázison kívülre, miközben a javított bejárás a kulcsot X*2-nél, az értéket X*2+1-nél olvassa
A hiba ritkán aktiválódott a /PageLabels-en, mert a címkeértékek szótárak, de egy számfa-helper, ami rossz lépésközzel olvas, abban a pillanatban romlásba megy, amint bármelyik érték szám, így mindkét bejárás most párokban lép

Címkék visszolvasása és oda-vissza vitelük

A TPDFlib.GetPageLabel(Page) egy 1-től induló oldal címkéjét adja vissza, és két tartalék viselkedése van, amit érdemes ismerni. Ha egyáltalán nincs /PageLabels bejegyzés, a decimális oldalszámot adja vissza, így a hívó feltétel nélkül használhatja. Ha van fa, de nincs az oldalt lefedő tartomány, üres stringet ad vissza, ami pontosan az, ami akkor történik, amikor egy fájl kihagyja a kötelező 0. indexű bejegyzést; a referenciadokumentáció szerint az 1. oldalon kezdődő tartománynak léteznie kell ahhoz, hogy a címkék helyesen jelenjenek meg, és a kód láthatóvá teszi ezt a követelményt. A betűstílusok a specifikációt követik, nem a táblázatoszlopokat: a Z után AA jön, aztán BB, a betű ismétlődik, nem átvitel történik

var
  P: Integer;
  Data: WideString;
begin
  // Gyors átvizsgálás, mit fog mutatni a megjelenítő az oldalablakában
  for P := 1 to Lib.PageCount do
    WriteLn(P, ' -> ', Lib.GetPageLabel(P));

  // A 4-es opcióérték csak a címketartományokat exportálja PageLabelBegin rekordokként
  Data := Lib.ExportDocumentData(4);
  // Az import a ClearPageLabels + AddPageLabels párosán játssza vissza őket
  Lib.ImportDocumentData(Data, 0);
end;

Tömeges szerkesztéshez az ExportDocumentData 4-es opcióértékkel minden tartományt PageLabelBegin blokként ír, PageLabelNewIndex, PageLabelStart, PageLabelPrefix és PageLabelNumStyle sorokkal, az ImportDocumentData pedig az első látott címkerekordot teljes cserének tekinti: egyszer meghívja a ClearPageLabels-t, majd minden rekordot a AddPageLabels-nek ad. Ez a szöveges oda-vissza utat determinisztikussá teszi akkor is, amikor az eredeti fájl /Kids fát használt, mert a törlés az egész katalógusbejegyzést leszedi, és az újraépített fa kezdettől fogva egyetlen levél

Mit sem garantál még mindig a javítás?

A kilapítás egyirányú, és bízik a megtalált sorrendben. Az EnumNumTree fáilsorrendben gyűjti a párokat, a GetPageLabel pedig azt az utolsó tartományt alkalmazza, aminek a kulcsa kisebb vagy egyenlő az oldalindexnél, így egy idegen fájl, aminek a levelei nincsenek sorrendben — amit a §7.9.7 tilt, de ami mégis kering — továbbra is adhat rossz címkéket, amíg újra nem építed a tartományokat ClearPageLabels-szal és friss AddPageLabels hívásokkal. A címkék oldalindexekhez kötődnek, nem oldalobjektumokhoz, így minden művelet, ami az oldalszámot vagy a sorrendet változtatja, a tartományokat a helyükön hagyja. Egy helyben végzett csere, mint a oldalak cseréje objektumszámok megőrzésével, megtartja a számot, ezért a címkék összhangban maradnak, míg egy összefésülés, mint a váltakoztatott duplex szkennelik összefésülése, új oldalsorrendet állít elő, ami frissen írt tartománysort érdemel

Az itt leírt oldalcímke hívások, a számfa-kezelés és a dokumentumadat export és import mind a PDF Library for Delphi részét képezik Delphi, C++Builder és Lazarus alatt, a AddPageLabels referencia-bejegyzése pedig dokumentálja a stílusértékeket és a visszatérési kódokat