Technický článek

Chyby kontroly rozsahu (Range Check) v PDF knihovnách pro Delphi: Hlavní příčiny

Chyby kontroly rozsahu v PDF knihovnách pro Delphi si vysloužily reputaci obtížně řešitelných problémů, protože nevykazují žádný konzistentní vzorec vstupů. U stejného dokumentu chyba nastane na jednom stroji, ale na jiném nikoliv; shodný procesní postup vyvolá výjimku u 3stránkového souboru, u dokumentu s 12 stránkami však proběhne čistě. Taková nepravidelnost se dá téměř vždy vystopovat k jediné hlavní příčině: objekty stránek (PDF page objects) nejsou uvnitř souboru uloženy vzestupně po sobě. Pokud knihovna vytvoří své vnitřní pole stránek tím způsobem, že nasnímá objekty postupně podle uložení namísto toho, aby sledovala strukturu (strom) stránek dle deklarace v katalogu (catalog), získá seznam stránek, jehož platný index se neshoduje s pořadím očekávaným uživatelem – a pak tato vestavěná kontrola chybí právě ve chvíli, kdy by se hodila nejvíce

Jak v Delphi funguje kontrola rozsahu

Když je aktivní překladačový direktiva {$R+} (ta je výchozí ve vývojovém režimu Debug configuration), Delphi RTL (Run-Time Library) během běhu programu zkontroluje a validuje každý index pole, každý řetězcový prvek a též případná přiřazení z výčtu. Krok "mimo stanovené hranice" automaticky vyvolá výjimku zvanou ERangeError, namísto toho, aby došlo k tichému překrývání v navazující paměti. Tohle chování si velmi ceníme: upozorní nás totiž na vzniklé skryté chyby na samém počátku, což účinně znemožní případné narušení podoby datové struktury, ke kterému by jinak mohlo v tichosti dojít třeba o celých sto řádků dále. Zneklidňující na výjimce však zůstává to, že vzniká na místě přístupu a nepochází od samotného bodu s chybně vypočítaným indexem. Pokud pak volací zásobník (call stack) odhalí hluboce vnořenou metodu kdesi v jedné z PDF jednotek, pak ona původní skutečná chyba zaručeně čeká už několik řádků (rámců) před tím

Vše se začne dále komplikovat složenými logickými podmínkami. Delphi zpravidla ohodnocuje výrazy and formou zleva doprava se zkrácenou logikou (short-circuit), nicméně krátké vyhodnocování automaticky přeruší zbývající řádek a vynechá evaluaci pouze tehdy, pokud má logický výraz na levé straně hodnotu False. Zápis typu:

if FDocStarted and (DestIndex < Length(PageArr)) and
   (PageArr[DestIndex].PageObj <> nil) then

Vypadá jako vysoce zabezpečený logický prvek, avšak ve skutečnosti chrání zápis před "přešlapy" mimo hranice pouze v případě, kdy FDocStarted vydá True a samotný parametr DestIndex nezíská hodnotu zápornou. Podmíněný logický odkaz DestIndex < Length(PageArr) nepřinese užitek ve chvíli, kdy DestIndex začne ukazovat na záporné číslo, jelikož porovnávání jakéhokoli záporného čísla proti logické (nezáporné) velikosti (Length) se ve světě znaménkové aritmetiky vrací jako True a navazující pole proto posléze obdrží (často mylný) přístup a tímto i vyvolá chybu ERangeError. Tento posun hraniční pojistky před rozsahem do té úplně nejzevnější pozice tak nabízí adekvátní řešení celého zmatku:

if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
  if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
    Result := PageArr[DestIndex].PageObj
  else
    Result := nil;
end
else
  raise ERangeError.CreateFmt(
    'Index stránky %d je mimo platný rozsah (0..%d)',
    [DestIndex, Length(PageArr) - 1]);

Tohle je samozřejmě řešení v rovině samotného programátora – oprava čistě na technické a mechanické úrovni. Podaří se díky ní zamezit vzniku systémových pádů. Nenabízí sice vysvětlení, proč hodnota parametru DestIndex už od samého začátku figurovala u čísla naprosto mimo platnou soustavu daného rozmezí, to však pro tyto případy stačí

Skutečná příčina: pořadí objektů vs pořadí stránek

Norma ISO 32000-1 §7.7.3 definuje vizuální postavení celého logického modelu jako tzv. strom objektů stránek (pages tree). Jde o vizuální a přesně ohraničený strom obsahující všechny prvky pole s formáty z takzvané tabulky (kolekce) Kids. Uvedený dokument v originále eviduje jednotlivé objekty do fyzického kódu podle nálady autora, a tak se pak docela snadno objeví třeba i prvek s formátem číslo 20 těsně před tím, než se fyzicky objeví formát u čísla 3 – vše uvnitř datového sledu jednotlivých bajtů. Knihovna od tvůrců, která buduje pořadník seznamu pro PDF podle pořadových čísel při jejich prolézání přes referenční systém na místo aby logicky následovala samotný řetězec pole označený jako parametr Kids, dodává čtenáři naprosto chaotický sled, který se vůbec neztotožňuje se zamýšleným pořadníkem celého dokumentu, u nějž pak tvůrci odhalují neočekávané výjimky u všech polí

Jediný možný správný a systematicky ideální princip při programování v této záležitosti totiž obnáší zahájit prolézání od katalogu souboru, převedení referenční části dokumentu pod složkou /Pages a následné rekurzivní procházení prvků uvnitř parametrů pole ze sekce Kids. Cesta prolézaným plochým logickým dokumentem s absolutní ztrátou všech formátů u uzlů nesoucích označení jako složka Pages, by se proto programátorovi představila logicky naprosto plynule v této následující formě:

procedure BuildPageIndexFromTree(
  const KidsArray: THPDFArray;
  var PageArr: TPageObjArray);
var
  i, Idx: Integer;
  Child: THPDFObject;
  ChildType: string;
begin
  for i := 0 to KidsArray.Count - 1 do
  begin
    Child := KidsArray.GetIndirectObject(i);
    if Child = nil then
      Continue;
    ChildType := Child.GetNameValue('/Type');
    if ChildType = 'Page' then
    begin
      Idx := Length(PageArr);
      SetLength(PageArr, Idx + 1);
      PageArr[Idx].PageObj := Child;
    end
    else if ChildType = 'Pages' then
    begin
      // mezilehlý uzel: přechod rekurzivně k dětským (Kids)
      BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
    end;
  end;
end;

Uvedený kód zajistí, že pole s formátem u parametru indexu u nuly tj. pod označením pro objekt se zápisem PageArr[0], ponese absolutní formální podobu první strany jakéhokoliv dokumentu u všech aplikovaných prohlížečů (nezávisle na svém logickém řazení uvnitř binárního toku u souborového bloku v samotných kódech (bajtových formátech) PDF). Parametrické zápisy pro indexy procházející do metodiky v domnění a očekávání řazení v "opravdovém" sledu stránek by od tohoto okamžiku nabízely plnou funkcionalitu u metod logického řazení souborů a tím by chybové výpisy o nedostatku platných hodnot pro nastavené rozpětí definitivně a napořád zanikly

Pevně nakódovaná okliková řešení problém jen znásobují

V programových blocích (codebase), kde nebylo nikdy objeveno základní a skutečné příčinné spojení k tomuto omylu, je možné nacházet hned celou řadu spíše takzvaných heuristických dodělávek: v případě celkového číselného znění o 3 počtech se přehodí formát u posledních hodnot stránek takříkajíc hned v samotném počátečním ohodnocování – případně se zavádí změna a úprava u indexu pro jednotlivé moduly pocházející od některého daného systému anebo pro změnu hned veškeré operace nabízejí ofset v případech, u kterých první načítaný logický rozsah indexového objektu u daného dokumentu už od začátku převyšuje požadovanou určenou hraniční mez. A pokaždé tento logický princip "doplňku" vychází u samotných autorských pokusů ze vzorově nakopírovaných příkladových referencí a chová se přesně podle toho jediného testovacího rozsahu. Zadejte novou řadu referenčních databázových seznamů pro PDF z jiných generátorových nástrojů na internetových formátech, kde pro změnu už dojde k aplikaci naprosto opačných opatření pro načítání z těchto zdrojů a celkový parametr se navíc obohatí už od počátku chybností ze zdvojnásobeného rozhraní: pole se jednak znovu zapsalo v naprosto chaotickém nesouladu k určeným hodnotám a na všechno ještě autoři do tohoto problému zavádějí i naprosto zhoubnou úroveň mapování. A co víc, hned obvyklá kontrola chybových záznamů pod systémem rozsahu to vše zachytí ještě kdesi dole ve spodní linii na pozadí výstupu a pro tento případ navíc stopy ohledně záznamových poloh s call stack formáty (příkazy ze stopy ve frontě se změnami v datech) do budoucna už naprosto s ničím hmatatelným nepomohou

Uvedená situace ovšem vyžaduje postup opačného charakteru. Zbavte tento model veškerých heuristických mapovacích "výpomocí" a plně nahraďte zhotovování záznamů pole pro všechny struktury u stránek spíše zcela novým, vhodným a korektně odzkoušeným přístupem se zpracováním záznamů v reálných stromech logického formátu a to napříč záznamy (tree walk). U pole tak bude zkrátka platit, že dokud probíhá operace u správných záznamů z hlediska kontextu logických struktur konstrukcí u čísel, nestává se u formátu chybovým ohrožením jakákoliv kontrolní výjimka proti hodnotovým překročením už dříve zmíněného "stanoveného řazení pro rozpětí". Stává se ale naprosto neškodnou, plně pomocnou a žádoucí oporou a nebude překážkou

Máte-li však nadále u samotné udržované a zhotovované logické knihovny pocit, že přítomnost problémů trvale ukazuje přesně na chybovost v takovém logickém spektru: zapněte přímo s volbami verze sestavy přes sekci "Release" chvilkově ohodnocování k samotnému prověření překročení stanovených dat pro rozsahy a spuštěním kontrol otestujte systém vůči všem pestřejším i logičtěji odlišným variacím PDF: vůči uceleným sadám od MS Word, od formátů jako LaTeX anebo pocházejících z nastavení ve firmware ze samotných systémů skenerů a od rozhraní nástrojů se štěpením systémů logického formátu (PDF-to-PDF). Běžně odhalíte nesrovnalosti zejména na souborech, které se neshodují formátem k vyžadovaným úkonům vůči logicky určeným procesům ve vaší aplikaci a knihovně. Je to právě zástup tohoto systému s rozložením objektových položek ve formátu stránek v PDF, který je pro chyby natolik důležitý – pro vaši aplikaci to poslouží jako cenná reference, ne se vždy obávat další separátní chyba systému k rozluštění

Co týče všech budoucích nově sepsaných kódů pro využití k referenčním bodům na knihovny Delphi a PDF moduly všeobecně. Obecně by však měl zafungovat docela prozaický návod: neochvějně považujte a evidujte výstup o hodnotách rozsahu od stránky z aplikované Delphi PDF knihovny za konečný i vysoce autoritativní dokument, přičemž do celého obsahu dbejte a doplňte prozatím vše na pravou míru s takovým pravidlem, že nedovolíte indexu posun do fáze aplikací vycházejících u samotných čísel od matematické reference "z vnější podoby dat" dříve, dokud ho neověříte uvnitř platného rámce soustavy v rozhraní u okrajů parametrů jako formátu: 0..PageCount - 1. Načtený soubor pro logické hodnoty u knihoven od sady komponenty HotPDF nabízí k nahlédnutí správnou kapacitu skrze formáty objektu u řady s celkovou strukturou stránek zprostředkováno ihned ze strany přístupu přes metodu pro funkci s vlastností THotPDF.PageCount po ukončení funkce typu BeginDoc a samozřejmě navíc pak automaticky pokaždé po obou logických systémech u nahrání a ukončení při vkládání – tato struktura z vlastností vždy naprosto spolehlivě odkazuje z hlediska formátu na hodnotu prvků ze zachycení k počtu stromového řazení a je tak ve finále docela bezpečné uvést samotnou veličinu s datem jako ideální a hlavně vrchní ochranný mantinel u samotných formátů pro kontrolní logiku do rozsahových mechanismů veškerých prozaických zkoušek z vícero aritmetických úkolů (operací u indexu)