A PDFlibPas úgy oldja fel a rekurzív Form XObject-hívásokat Delphi PDF-tartalomfolyamokban, hogy az aktív hívási láncot követi, nem egy globális látogatott-halmazt, így a TPDFlib.EnumPageContentStatesEx be tudja járni ugyanazt a Formot, amit többször hívnak meg egy oldalon, anélkül hogy összekeverné a legitim újrahasznosítást egy ciklussal. Egy bélyegző Form XObject egy számlasablonban a tipikus eset: ugyanazt az objektumot hívja meg a fejléc, a lábléc, és egy vízjel-réteg egy oldalon, és csak egy hívási lánc, amely visszahurkolódik önmagára, valódi ciklus
Az ISO 32000-1 §8.10 egy Form XObjectet önálló tartalomfolyamként definiál, amit egy oldal, vagy egy másik Form, a Do operátorral hív meg, saját koordinátarendszerrel a /Matrix-ban, egy vágási határral abban a koordinátarendszerben a /BBox-ban, és opcionálisan saját erőforrás-szótárral. Semmi a specifikációban nem korlátozza, hányszor hívható meg egy Form, vagy milyen mélyen hívhatják egymást a Formok, így egy szabványkövető elemzőnek el kell fogadnia a legitim újrahasznosítást és a legitim ágyazást, miközben mégis védekezik az egyetlen elrendezés ellen, amit a specifikáció ténylegesen tilt: egy Form, amelynek tartalomfolyama, közvetlenül vagy tranzitíven, önmagát hívja. A PDFlibPas ezt a megkülönböztetést a TPDFlibContentFormTraversalStatus értékeken keresztül jelenti, amelyek minden Do pillanatképhez csatoltak, leginkább az ftsEnumerated-en keresztül egy sikeres leszálláshoz, és az ftsCycle-ön keresztül az egyetlen esethez, amely ténylegesen egy hurok
Miért nem vált ki hamis ciklust ugyanannak a Form XObjectnek az újrahasznosítása?
Egy ismétlődő Form XObject-hivatkozás önmagában nem bizonyíték semmi rosszra. Az ISO 32000-1 megengedi, hogy ugyanaz a Form-objektum egy tartalomfolyamon belül annyi helyről legyen meghívva, ahányat a szerző akar, ami pontosan az, ahogyan egy logóbélyegző, egy fejléces sablon, vagy egy oldalszám-lábléc újrahasznosításra kerül egy oldalon anélkül, hogy tartalomfolyamát többször duplikálnák. A naiv védekezés az elszabaduló rekurzió ellen egyetlen látogatott-halmaz, objektumszám szerint kulcsolva: amikor egy bejáró először látja a 12-es Form-objektumot, megjelöli a 12-t látottként, és megtagadja, hogy máshol újra belépjen a fába. Ez a megközelítés abban a pillanatban eltörik, amikor ugyanaz a bélyegző egy oldal két, egymással össze nem függő sarkában jelenik meg, mert a második, teljesen legitim hívás azután érkezik, hogy az objektumszámot már látottnak jelölték, és ciklusként utasítják el, pedig nem az
A PDFlibPas elkerüli ezt a hamis pozitívot azáltal, hogy a ciklusfelismerést az aktuális hívási láncra korlátozza, nem a teljes dokumentumra. Az EnumPageContentStatesEx beteszi a feloldott Form-folyamot az aktív hívási láncba közvetlenül azelőtt hogy belépne, majd lehúzza ugyanazt a bejegyzést, amint a leszállás visszatér, sikeresen vagy sem. Egy testvér-hívás az azonos folyamhoz csak azután indul, hogy az elsőt már lehúzták, így a hívási lánc tiszta attól a folyamtól, mire a testvér-hívás ellenőrzi, és a bejáró pontosan úgy sorolja fel, ahogyan bármely más Formot tenné. Egy valódi ciklus máshogy néz ki ugyanazon a láncon: A Form meghívja a B Formot, a B még nyitva van a láncon, amikor saját tartalma visszahívja az A-t, és az A még mindig ott ül a láncon a külső hívásból, amely még nem tért vissza — ez az egyetlen alak, amit az ftsCycle jelent, egy Form-folyam, amely még mindig nyitva van valahol korábban az aktuális hívási láncon, nem csupán jelen van valahol máshol az oldalon
Milyen mélyen mehet a Form XObject-rekurzió, mielőtt a PDFlibPas megállítja?
A ciklusfelismerés és a mélységkorlátozás két különböző problémát old meg, és a PDFlibPas pontosan emiatt tartja őket két különböző TPDFlibContentFormTraversalStatus eredményként. Egy húsz különálló Formból álló lánc, mindegyik meghívva a következőt, és egyik sem ismétlődik, semmilyen definíció szerint nem ciklus — az aktív-lánc-ellenőrzés soha nem talál ismétlődő folyamot —, de húsz becsületes ágyazási szint még mindig húsz szintnyi elemzés, mátrix-konkatenáció, és erőforrás-feloldás, amit egy hibás formátumú vagy ellenséges PDF önkényesen magasabbra tolhat, ha semmi más nem állítja meg. Az EnumPageContentStatesEx pontosan emiatt vesz fel egy MaxFormDepth paramétert, és bármilyen értéket is adnak át, legfeljebb 64-re szorítja, függetlenül attól, mit kér a hívó. A nulla mélység egy külön ismerésre érdemes speciális eset: teljesen kikapcsolja a Form-rekurziót, és reprodukálja a régebbi EnumPageContentStates metódus lapos, csak-oldal viselkedését, ezért minden Do pillanatkép abban a módban ftsNotRequested-et jelent ahelyett hogy bármit is megkísérelne
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;
Egy alnyomkövető meghívásonként: a grafikai állapot elszigetelése
Minden leszállás egy Form XObjectbe saját grafikaiállapot-nyomkövetőt kap, ahelyett hogy megosztaná az oldalt már bejáró nyomkövetőt, mert egy Form tartalomfolyama köteles pontosan úgy hagyni a grafikai állapotot, ahogyan találta, és a PDFlibPas nem feltételezheti, hogy minden PDF, amit megnyit, ténylegesen tiszteletben tartja ezt a követelményt. A gyerek nyomkövető egy pillanatképből indul arról, milyen CTM, színállapot, és szövegparaméterek voltak aktívak a hívó Do utasításnál, majd visszaállítja saját mentés-és-visszaállítás vermét és aktuálisútvonal-nyomkövetését üresre, mielőtt végrehajtaná a Form egyetlen utasítását is. Egy kiegyensúlyozatlan q, hozzá tartozó Q nélkül egy gondatlan vagy sérült Formon belül, nem ritka dolog régebbi eszközök által előállított PDF-ekben, azon az egy meghíváson belüli nyomkövetőn belül marad, és soha nem szivárog ki az oldal-nyomkövetőbe, vagy ugyanazon bélyegző testvér-meghívásába, amely egy sorral később ül a tartalomfolyamban
A Form /Matrix-a ugyanúgy komponálódik a Do-nál érvényes CTM-mel, mint egy cm operátor, balról szorozva az aktuális transzformációval, nem helyettesítve azt, és a PDFlibPas szándékosan újrahasznosítja ezt az egy kódútvonalat, ahelyett hogy egy második képletet tartana fenn, mivel ugyanannak a mátrix-algebrának két független megvalósítása pontosan az a fajta duplikáció, amely csendben szétdriftel néhány kör skálázás, forgatás, és nyírás-kompozíció után. A /BBox ezután a Form saját koordinátaterében vág, miután a mátrixot már alkalmazták, és annak a doboznak mind a négy sarka egyenként transzformálódik, nem csak az ellentétes sarkok, mivel egy elforgatott vagy nyírt Form egyébként olyan határolódobozt jelenthet, amely elmulaszt valódi tartalmat, amely abban ül, ami egykor egy szélső sarok volt, mielőtt a transzformáció máshová mozgatta. A ciklus kiterjesztése az előző példából ugyanazon a States tömbön keresztül közvetlenül olvassa be ezeket a mezőket
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);
Két Form ugyanazzal az erőforrásnévvel megoszt egy betűtípust?
Nem. Egy erőforrásnév, mint a /F1, csak azon erőforrás-szótár viszonylatában jelent valamit, amely aktív azon a ponton, ahol használják, és két különböző Form XObject szabadon definiálhat két teljesen eltérő betűtípust ugyanazon azonos név alatt. A PDFlibPas ezt úgy oldja meg, hogy egy erőforrás-hatókört követ minden erőforrásnévvel együtt: amikor egy Form saját /Resources szótárat hordoz, az a szótár válik a teljes erőforrás-hatókörré minden számára, ami belül van, kulcsonkénti visszaesés nélkül az oldal vagy hívó szótárára bármire, amit a Form saját szótára véletlenül kihagy. Csak egy Form, amelynek egyáltalán nincs /Resources kulcsa, egy minta, amit néhány régebbi PDF-generátor még mindig előállít, örökli teljes egészében a hívó szótárt, és ez egy szándékos kompatibilitási kivétel, nem egy általános szabály, amire új kimenetben érdemes támaszkodni. A betűtípus-azonosság egy TPDFlibContentGraphicsState pillanatképben ezért a FontResource és FontResourceScope párja, nem csak a név önmagában, a FontObjectNumber-rel elérhetően, hogy megerősítse, pontosan melyik közvetett objektumra oldódott fel egy adott /F1 abban a konkrét hatókörben
Ugyanez a hatókörözés vonatkozik minden más nevezett erőforrásra, amit egy Form hordozhat, az ExtGState bejegyzéseket és az ágyazott XObject bejegyzéseket is beleértve, mivel az alapul szolgáló feloldási mechanizmus nem kezeli speciális esetként a betűtípusokat — a betűtípus-eset véletlenül csak a leginkább számít, mert egy összekevert betűtípus-azonosság csendben rossz glyph-eket állít elő, nem egy nyilvánvaló hibát. Kinyerő kód, amely csak betűtípusnév szerint csoportosítja a szövegfutamokat, erőforrás-hatókör szerinti csoportosítás nélkül, összeolvaszt két vizuálisan eltérő betűtípust, amelyek véletlenül egy nevet osztanak meg, és a hiba nem bukkan fel, amíg valaki észre nem veszi a rossz betűtípusból származó számjegyeket olyasmiben, aminek egy konzisztens betűtípusként kellett volna olvasódnia
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);
A FormTraversalStatus olvasása a saját csővezetékedben
A FormTraversalStatus minden Do pillanatképet önmagában kis diagnosztikai jelentéssé alakít, és egy csővezeték, amely figyelmen kívül hagyja, pontosan azt az információt dobja el, amely megmagyarázná egy hiányos kinyerést. Az ftsNotApplicable azt jelenti, hogy az utasítás soha nem volt eleve feloldott Form-hívás; az ftsNotRequested azt jelenti, hogy a rekurzió ki volt kapcsolva ehhez a híváshoz; az ftsEnumerated azt jelenti, hogy a Formot sikeresen elemezték és bejárták; az ftsDepthLimit és az ftsCycle a két módot jelöli, ahogyan egy leszállás szándékosan félbeszakad; és az ftsMalformed mindent lefed mást, ami megállította a bejárást — egy feloldhatatlan folyam-hivatkozás, egy /Matrix vagy /BBox, amely nem elemezhető, vagy egy kivétel, amit a Form saját tartalmának végrehajtása közben dobtak. Ez az utolsó eset operatívan számít, mert egy elbukott ágyazott bejárás visszagörgeti bármilyen részleges kimenetet, amit már előállított ahhoz az ághoz, így egy hívónak soha nem kell találgatnia, egy Form valóban üres volt-e, vagy egyszerűen felrobbant két utasítással a tartalomfolyamába
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;
Határok, költségek, és hova illik ez
Egy Form tartalomfolyamát pontosan egyszer dekódolják és elemzik enumerálási hívásonként, függetlenül attól, hányszor hívják meg a Formot, mert a PDFlibPas gyorsítótárazza az elemzett utasításlistát az alapul szolgáló folyamobjektum ellen, ahelyett hogy minden testvér-híváskor újraelemzené — a nyitó példa háromsarkos bélyegzőjét egyszer dekódolják, és háromszor járják be, nem háromszor dekódolják. Ami ténylegesen újraépül minden egyes meghíváskor, az minden, ami legitim módon eltér egyik híváshelytől a másikig: a gyerek-nyomkövető, a konkatenált CTM, a metszett vágás, és az erőforrás-hatókör. Ugyanaz a meghívásonkénti CTM- és vágás-könyvelés az a gépezet, amely a PDFlibPas tartalomfolyam CTM- és vágás-állapot-nyomkövetője mögött áll, érdemes vele együtt olvasni bármely olyan tartalomfolyam-bejáráshoz, amely túlmutat magán a Form-rekurzión
Két korlátot érdemes elvárásokként beállítani, mielőtt ez az API egy nagyobb csővezetékbe kerül. A 64 szintes mélységplafon nem egy hangolókar legitim mély dokumentumokhoz, mivel a valódi számlák, kimutatások, és jelentéssablonok gyakorlatilag soha nem ágyaznak Formokat három vagy négy szintnél mélyebbre — egy dokumentum, amely ténylegesen eltalálja az ftsDepthLimit-et, sokkal valószínűbb, hogy hibás formátumú vagy ellenséges, mint szokatlanul kidolgozott, és érdemes adatminőségi jelzésként naplózni, nem csendben újrapróbálni nagyobb számmal. Az EnumPageContentStatesEx egy olvasási-oldali elemző API is: azt jelenti, mit csinál egy tartalomfolyam, nem azt, hogy egy Formnak látszania kellene-e egyáltalán, ami egy külön kérdés, amelyre az Optional Content Group láthatósági állapot ad választ, amikor egy bélyegző vagy vízjel Form egy réteg mögött ül, amit egy megjelenítő esetleg kikapcsolt. A hívási-lánc ciklusfelismerés, a meghívásonkénti elszigetelés, és az erőforrás-hatókörözés együtt teszik ki a tartalomfolyam-vizsgálati felület egy sarkát a Delphihez és C++Builderhez készült PDFlibPas komponensben