PDFlibPas rešava rekurzivne pozive Form XObject objekata u Delphi PDF tokovima sadržaja praćenjem aktivnog lanca poziva, a ne globalnog skupa posećenih objekata, pa TPDFlib.EnumPageContentStatesEx može da prođe kroz isti Form koji je pozvan više puta na jednoj stranici, a da legitimnu ponovnu upotrebu ne zameni za ciklus. Tipičan primer je Form XObject pečata u šablonu fakture: isti objekat se poziva iz zaglavlja, podnožja i sloja vodenog žiga na jednoj stranici, a samo lanac poziva koji se vraća sam na sebe predstavlja pravi ciklus
ISO 32000-1 §8.10 definiše Form XObject kao samostalni tok sadržaja koji stranica ili drugi Form poziva operatorom Do, sa sopstvenim koordinatnim sistemom u /Matrix, granicom odsecanja u tom koordinatnom sistemu u /BBox i, opciono, sopstvenim rečnikom resursa. Specifikacija ne ograničava koliko puta jedan Form može biti pozvan niti koliko duboko Form objekti mogu pozivati jedni druge, zato parser koji je usklađen sa standardom mora da prihvati legitimnu ponovnu upotrebu i legitimno ugnježđivanje, a da se istovremeno zaštiti od jedine konstrukcije koju specifikacija zabranjuje: Form objekta čiji tok sadržaja, direktno ili posredno, poziva samog sebe. PDFlibPas tu razliku prijavljuje kroz vrednosti TPDFlibContentFormTraversalStatus pridružene svakom snimku operatora Do, naročito kroz ftsEnumerated za uspešan silazak i ftsCycle za jedini slučaj koji je zaista petlja
Zašto ponovna upotreba istog Form XObject objekta ne izaziva lažni ciklus?
Ponavljana referenca na Form XObject sama po sebi nije dokaz da nešto nije u redu. ISO 32000-1 dozvoljava da isti Form objekat bude pozvan iz onoliko mesta u toku sadržaja koliko autor želi, upravo kao što se pečat sa logotipom, šablon zaglavlja ili podnožje sa brojem stranice ponovo koriste na stranici bez višestrukog umnožavanja njihovog toka sadržaja. Naivna zaštita od nekontrolisane rekurzije jeste jedan skup posećenih objekata indeksiran brojem objekta: kada prolaznik prvi put vidi Form objekat 12, označi 12 kao posećen i odbija da ponovo uđe u njega bilo gde drugde u stablu. Takav pristup se kvari čim se isti pečat pojavi u dva nepovezana ugla jedne stranice, jer drugi, potpuno legitiman poziv stiže nakon što je broj objekta već označen kao posećen i biva odbijen kao da je petlja
PDFlibPas izbegava taj lažno pozitivan rezultat tako što otkrivanje ciklusa vezuje za trenutni lanac poziva umesto za ceo dokument. EnumPageContentStatesEx ubacuje razrešeni Form tok u aktivni lanac poziva neposredno pre silaska u njega, a zatim isti zapis uklanja čim se silazak završi, uspešno ili ne. Poziv identičnog toka iz susedne grane počinje tek nakon što je prvi zapis već uklonjen, pa je lanac poziva prazan za taj tok kada ga susedni poziv proveri i prolaznik ga obrađuje upravo kao bilo koji drugi Form. Pravi ciklus izgleda drugačije na istom lancu: Form A poziva Form B, B je i dalje otvoren na lancu kada njegov sadržaj ponovo pozove A, a A i dalje sedi na lancu iz spoljnog poziva koji se još nije vratio — to je jedini oblik koji ftsCycle prijavljuje, Form tok koji je još otvoren negde ranije u trenutnom lancu poziva, a ne samo prisutan negde drugde na stranici
Koliko duboko može da ide rekurzija Form XObject objekata pre nego što je PDFlibPas zaustavi?
Otkrivanje ciklusa i ograničavanje dubine rešavaju dva različita problema, a PDFlibPas ih upravo zato zadržava kao dva različita ishoda TPDFlibContentFormTraversalStatus. Lanac od dvadeset različitih Form objekata, gde svaki poziva sledeći i nijedan se ne ponavlja, po bilo kojoj definiciji nije ciklus — provera aktivnog lanca nikada ne pronalazi ponovljeni tok — ali dvadeset stvarnih nivoa ugnježđivanja i dalje znači dvadeset nivoa parsiranja, ulančavanja matrica i razrešavanja resursa koje bi neispravan ili zlonameran PDF mogao proizvoljno da poveća ako ga ništa drugo ne zaustavi. EnumPageContentStatesEx zato prima parametar MaxFormDepth i ograničava prosleđenu vrednost na najviše 64, bez obzira na ono što pozivalac zahteva. Dubina nula je poseban slučaj koji vredi zapamtiti: ona potpuno isključuje rekurziju Form objekata i vraća ravno ponašanje starije metode EnumPageContentStates, koja obrađuje samo stranicu, zbog čega svaki snimak operatora Do u tom režimu prijavljuje ftsNotRequested umesto da pokuša bilo šta
var
Lib: TPDFlib;
States: array of TPDFlibContentGraphicsState;
Count, I: Integer;
begin
Lib:= TPDFlib.Create;
try
if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
Exit;
Lib.SelectPage(1);
Count:= Lib.EnumPageContentStatesEx(True, 8, States); // count only
SetLength(States, Count);
Lib.EnumPageContentStatesEx(True, 8, States); // fill
for I:= 0 to Count- 1 do
if States[I].FormTraversalStatus= ftsCycle then
LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
finally
Lib.Free;
end;
end;
Jedan podpratilac po pozivu: izolovanje grafičkog stanja
Svaki silazak u Form XObject dobija sopstvenog pratioca grafičkog stanja umesto da deli pratioca koji već prolazi kroz stranicu, jer tok sadržaja Form objekta mora da ostavi grafičko stanje tačno onakvim kakvim ga je zatekao, a PDFlibPas ne može da pretpostavi da svaki PDF koji otvori zaista poštuje taj zahtev. Podređeni pratilac počinje od snimka aktivnih CTM, stanja boje i tekstualnih parametara u pozivajućoj instrukciji Do, zatim pre izvršavanja jedne instrukcije Form objekta prazni sopstveni stek čuvanja i vraćanja i praćenje trenutne putanje. Neuravnoteženi q bez odgovarajućeg Q unutar nemarnog ili oštećenog Form objekta, što nije retkost u PDF-ovima koje je proizveo stariji alat, ostaje ograničen na pratioca tog jednog poziva i nikada ne curi u pratioca stranice niti u susedni poziv istog pečata koji se nalazi u sledećem redu toka sadržaja
Form /Matrix se sastavlja sa CTM-om aktivnim pri operatoru Do na isti način kao operator cm, množenjem sleva sa trenutnom transformacijom umesto zamenjivanja njome, a PDFlibPas namerno ponovo koristi taj isti put koda umesto održavanja druge formule, jer su dve nezavisne implementacije iste matrične algebre upravo vrsta dupliranja koja se neprimetno raziđe nakon nekoliko uzastopnih skaliranja, rotacija i smicanja. /BBox zatim odseca u sopstvenom koordinatnom prostoru Form objekta nakon primene matrice, a sva četiri ugla tog pravougaonika transformišu se pojedinačno umesto samo suprotnih uglova, jer rotirani ili posmični Form inače može da prijavi granični okvir koji propušta stvarni sadržaj što je pre transformacije bio u krajnjem uglu, a transformacija ga pomerila negde drugde. Proširivanje petlje iz prethodnog primera preko istog niza States direktno čita ta polja
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
States[I].FormBBoxKnown then
Writeln('Form ', States[I].XObjectResource, ' matrix ',
States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);
Da li dva Form objekta sa istim imenom resursa dele isti font?
Ne. Ime resursa kao što je /F1 ima značenje samo u odnosu na rečnik resursa aktivan na mestu upotrebe, a dva različita Form XObject objekta mogu slobodno da definišu dva potpuno različita fonta pod tim istim imenom. PDFlibPas to rešava praćenjem opsega resursa uz svako ime resursa: kada Form ima sopstveni rečnik /Resources, taj rečnik postaje potpuni opseg resursa za sve unutar njega, bez vraćanja po pojedinačnom ključu na rečnik stranice ili pozivaoca za ono što sopstveni rečnik Form objekta eventualno izostavi. Samo Form bez ključa /Resources, obrazac koji još proizvode neki stariji PDF generatori, nasleđuje pozivajući rečnik u celini, a to je namerni izuzetak radi kompatibilnosti, ne opšte pravilo na koje treba računati u novom izlazu. Identitet fonta u snimku TPDFlibContentGraphicsState zato čini par FontResource i FontResourceScope, a ne samo ime, dok je FontObjectNumber dostupan da potvrdi tačno na koji indirektni objekat je određeni /F1 razrešen u tom konkretnom opsegu
Isto ograničavanje opsegom važi za svaki drugi imenovani resurs koji Form može da sadrži, uključujući stavke ExtGState i ugnježđene stavke XObject, jer mehanizam razrešavanja ne pravi poseban slučaj za fontove — slučaj fonta je samo najvažniji, zato što neusklađen identitet fonta neprimetno proizvodi pogrešne glifove umesto očigledne greške. Kod za izdvajanje koji grupiše tekstualne nizove samo po imenu fonta, bez grupisanja i po opsegu resursa, spojiće dva vizuelno različita fonta koja slučajno dele ime, a greška će se pokazati tek kada neko primeti cifre iz pogrešnog pisma unutar teksta koji je trebalo da koristi jedan dosledan font
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
States[I].FontObjectNumber, States[I].ContentDepth);
Čitanje FormTraversalStatus u sopstvenom procesu
FormTraversalStatus pretvara svaki snimak operatora Do u mali samostalni dijagnostički izveštaj, a proces koji ga zanemari odbacuje upravo informacije koje bi objasnile nepotpuno izdvajanje. ftsNotApplicable znači da instrukcija od početka nije bila razrešeni poziv Form objekta; ftsNotRequested znači da je rekurzija isključena za taj poziv; ftsEnumerated znači da je Form uspešno parsiran i obrađen; ftsDepthLimit i ftsCycle označavaju dva načina na koja se silazak namerno prekida; a ftsMalformed obuhvata sve ostalo što je zaustavilo prolazak — nerešivu referencu na tok, /Matrix ili /BBox koji nisu mogli da se parsiraju ili izuzetak podignut tokom izvršavanja sopstvenog sadržaja Form objekta. Taj poslednji slučaj je operativno važan, jer neuspešan ugnježđeni prolazak vraća sve delimične rezultate koje je već proizveo za tu granu, pa pozivalac nikada ne mora da nagađa da li je Form bio zaista prazan ili se jednostavno srušio dve instrukcije nakon početka svog sadržaja
var
Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
Status: TPDFlibContentFormTraversalStatus;
begin
for Status:= Low(Tally) to High(Tally) do
Tally[Status]:= 0;
for I:= 0 to Count- 1 do
Inc(Tally[States[I].FormTraversalStatus]);
if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;
Granice, troškovi i mesto u celini
Tok sadržaja Form objekta dekodira se i parsira tačno jednom po pozivu enumeracije, bez obzira na to koliko puta je Form pozvan, jer PDFlibPas kešira listu parsiranih instrukcija uz osnovni objekat toka umesto da je ponovo parsira pri svakom susednom pozivu — pečat sa tri ugla iz uvodnog primera dekodira se jednom i obrađuje tri puta, a ne dekodira tri puta. Ono što se ponovo gradi pri svakom pojedinačnom pozivu jeste sve što se legitimno razlikuje između jednog mesta poziva i sledećeg: podređeni pratilac, spojeni CTM, presek odsecanja i opseg resursa. To praćenje CTM-a i odsecanja po pozivu koristi isti mehanizam kao pratilac CTM-a i stanja odsecanja toka sadržaja PDFlibPas-a, koji vredi pročitati uz ovaj tekst za svaki prolazak kroz tok sadržaja koji ide dalje od same rekurzije Form objekata
Dve granice treba uzeti u obzir pre uključivanja ovog API-ja u veći proces. Ograničenje dubine od 64 nivoa nije podešavanje za legitimno duboke dokumente, jer se stvarne fakture, izveštaji i šabloni izjava gotovo nikada ne ugnježđuju dublje od tri ili četiri nivoa — dokument koji zaista dostigne ftsDepthLimit mnogo je verovatnije neispravan ili zlonameran nego neobično složen i vredi ga evidentirati kao signal kvaliteta podataka umesto ga nečujno ponavljati sa većim brojem. EnumPageContentStatesEx je takođe API za analizu pri čitanju: izveštava šta tok sadržaja radi, a ne da li Form uopšte treba da bude vidljiv, što je zasebno pitanje na koje odgovara stanje vidljivosti Optional Content Group objekta kada se Form pečata ili vodenog žiga nalazi iza sloja koji je gledalac možda isključio. Otkrivanje ciklusa lancem poziva, izolovanje po pozivu i ograničavanje opsegom resursa zajedno čine jedan deo površine za pregled tokova sadržaja u komponenti PDFlibPas za Delphi i C++Builder