Tehnički članak

Form XObject u PDF-u: otkrivanje rekurzivnih ciklusa

PDFlibPas razrješava rekurzivne pozive Form XObject objekata u Delphi PDF tokovima sadržaja praćenjem aktivnog lanca poziva, a ne globalnog skupa posjećenih objekata, pa TPDFlib.EnumPageContentStatesEx može proći kroz isti Form koji je na jednoj stranici pozvan više puta, a da legitimnu ponovnu upotrebu ne zamijeni za ciklus. Form XObject s pečatom u predlošku računa tipičan je slučaj: isti se objekt poziva iz zaglavlja, podnožja i sloja vodenog žiga na jednoj stranici, a pravi je ciklus samo lanac poziva koji se vraća sam na sebe

ISO 32000-1 §8.10 definira Form XObject kao samostalan tok sadržaja koji stranica ili drugi Form poziva operatorom Do, s vlastitim koordinatnim sustavom u /Matrix, granicom isijecanja u tom koordinatnom sustavu u /BBox i, po želji, vlastitim rječnikom resursa. Specifikacija ne ograničava koliko se puta jedan Form smije pozvati ni koliko duboko Form objekti smiju pozivati jedan drugoga, pa parser koji je usklađen sa specifikacijom mora prihvatiti legitimnu ponovnu upotrebu i legitimno ugniježđivanje, a istodobno se zaštititi od jedinog rasporeda koji specifikacija zabranjuje: Form objekta čiji tok sadržaja izravno ili posredno poziva samoga sebe. PDFlibPas tu razliku prijavljuje kroz vrijednosti TPDFlibContentFormTraversalStatus pridružene svakom snimku operatora Do, ponajprije ftsEnumerated za uspješan ulazak i ftsCycle za jedini slučaj koji je doista petlja

Zašto ponovna upotreba istog Form XObject objekta ne izaziva lažni ciklus?

Ponovljena referenca na Form XObject sama po sebi nije dokaz da nešto nije u redu. ISO 32000-1 dopušta autoru da isti Form objekt pozove s koliko god mjesta u toku sadržaja želi, upravo onako kako se pečat s logotipom, predložak zaglavlja ili podnožje s brojem stranice ponovno upotrebljavaju na stranici bez višestrukog kopiranja njihova toka sadržaja. Naivna zaštita od nezaustavljive rekurzije jest jedan skup posjećenih objekata indeksiran brojem objekta: kada obilaznik prvi put vidi Form objekt 12, označi 12 kao viđen i odbija ponovno ući u njega bilo gdje drugdje u stablu. Taj pristup puca čim se isti pečat pojavi u dva nepovezana kuta jedne stranice, jer drugi, potpuno legitiman poziv dolazi nakon što je broj objekta već označen kao viđen pa biva odbijen kao da je petlja

PDFlibPas izbjegava taj lažni pozitivan rezultat tako što otkrivanje ciklusa ograničava na trenutačni lanac poziva, a ne na cijeli dokument. EnumPageContentStatesEx neposredno prije ulaska u razriješeni Form tok dodaje taj tok u aktivni lanac poziva, a zatim isti unos uklanja čim se ulazak vrati, uspješno ili ne. Poziv istog toka iz susjedne grane počinje tek nakon što je prvi unos već uklonjen, pa je lanac poziva prazan za taj tok kada ga susjedni poziv provjeri i obilaznik ga obrađuje upravo kao bilo koji drugi Form. Pravi ciklus izgleda drukčije na tom istom lancu: Form A poziva Form B, B je još otvoren u lancu kada njegov sadržaj ponovno pozove A, a A još uvijek stoji u lancu zbog vanjskog poziva koji se nije vratio — to je jedini oblik koji ftsCycle prijavljuje, Form tok koji je još otvoren negdje ranije u trenutačnom lancu poziva, a ne samo prisutan negdje drugdje na stranici

Koliko duboko rekurzija Form XObject objekata može ići prije nego što je PDFlibPas zaustavi?

Otkrivanje ciklusa i ograničavanje dubine rješavaju dva različita problema, pa ih PDFlibPas upravo zato drži kao dva različita ishoda TPDFlibContentFormTraversalStatus. Lanac od dvadeset različitih Form objekata, gdje svaki poziva sljedeći i nijedan se ne ponavlja, nije ciklus ni po jednoj definiciji — provjera aktivnog lanca nikada ne pronalazi ponovljeni tok — ali dvadeset stvarnih razina ugniježđenosti i dalje znači dvadeset razina parsiranja, spajanja matrica i razrješavanja resursa koje bi neispravan ili zlonamjeran PDF mogao proizvoljno povećavati kada ga ništa drugo ne bi zaustavilo. EnumPageContentStatesEx zato prima parametar MaxFormDepth i ograničava svaku proslijeđenu vrijednost na najviše 64, bez obzira na ono što pozivatelj zatraži. Dubina nula poseban je slučaj koji vrijedi zapamtiti: potpuno onemogućuje rekurziju Form objekata i vraća ravno, samo-stranično ponašanje starije metode EnumPageContentStates, zbog čega svaki snimak operatora Do u tom načinu prijavljuje ftsNotRequested umjesto da išta pokušava

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 podpraćitelj po pozivu: izoliranje grafičkog stanja

Svaki ulazak u Form XObject dobiva vlastiti praćitelj grafičkog stanja umjesto da dijeli onaj koji već obilazi stranicu, jer tok sadržaja Form objekta mora ostaviti grafičko stanje točno onakvim kakvo je zatekao, a PDFlibPas ne može pretpostaviti da svaki PDF koji otvori doista poštuje taj zahtjev. Dječji praćitelj počinje snimkom trenutačnog CTM-a, stanja boje i tekstualnih parametara aktivnih na pozivajućoj uputi Do, a zatim prije izvršavanja prve upute Form objekta vlastiti stog spremanja i vraćanja te praćenje trenutačne putanje postavlja na prazno. Neuravnoteženi operator q bez odgovarajućeg Q unutar nemarnog ili oštećenog Form objekta, što nije rijetkost u PDF-ovima koje je proizveo stariji alat, ostaje ograničen na praćitelj tog jednog poziva i nikada ne prodire u praćitelj stranice ni u susjedni poziv istog pečata koji se nalazi jedan redak kasnije u toku sadržaja

Form /Matrix sastavlja se s CTM-om aktivnim na operatoru Do na isti način kao operator cm, množenjem slijeva s trenutačnom transformacijom umjesto zamjene za nju, a PDFlibPas namjerno ponovno upotrebljava isti put koda umjesto održavanja druge formule, jer se dvije neovisne implementacije iste matrične algebre nakon nekoliko kombinacija skaliranja, rotacije i nagiba upravo lako neprimjetno raziđu. /BBox zatim isijeca u vlastitom koordinatnom prostoru Form objekta nakon primjene matrice, a sva četiri kuta tog okvira transformiraju se pojedinačno umjesto samo suprotnih kutova, jer rotirani ili nagnuti Form inače može prijaviti okvir koji promašuje stvarni sadržaj smješten u kutu koji je prije transformacije bio krajnji, a nakon nje se pomaknuo drugamo. Proširivanje petlje iz prethodnog primjera preko istog polja States izravno č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);

Dijele li dva Form objekta s istim nazivom resursa jedan font?

Ne. Naziv resursa kao što je /F1 ima značenje samo u odnosu na rječnik resursa aktivan na mjestu njegove upotrebe, a dva različita Form XObject objekta slobodno mogu pod tim istim nazivom definirati dva potpuno različita fonta. PDFlibPas to razrješava praćenjem dosega resursa uz svaki naziv resursa: kada Form ima vlastiti rječnik /Resources, taj rječnik postaje potpuni doseg resursa za sve unutar njega, bez povratnog traženja pojedinačnih ključeva u rječniku stranice ili pozivatelja za ono što vlastiti rječnik Form objekta slučajno ne sadrži. Samo Form kojemu potpuno nedostaje ključ /Resources, obrazac koji još proizvode neki stariji generatori PDF-a, nasljeđuje cijeli rječnik pozivatelja, a to je namjerna iznimka radi kompatibilnosti, a ne opće pravilo na koje se treba oslanjati u novom izlazu. Identitet fonta u snimci TPDFlibContentGraphicsState stoga je par FontResource i FontResourceScope, a ne samo naziv, dok je FontObjectNumber dostupan za potvrdu točno onog neizravnog objekta na koji se određeni /F1 razriješio u tom dosegu

Isto ograničavanje dosega primjenjuje se na svaki drugi imenovani resurs koji Form može sadržavati, uključujući unose ExtGState i ugniježđene unose XObject, jer temeljni mehanizam razrješavanja ne izdvaja fontove — slučaj fonta samo je najvažniji jer pogrešan identitet fonta tiho proizvodi pogrešne glifove umjesto očite pogreške. Kod za izdvajanje koji grupira tekstualne nizove samo prema nazivu fonta, bez grupiranja i prema dosegu resursa, spojit će dva vizualno različita fonta koja slučajno dijele naziv, a pogreška će se pokazati tek kada netko primijeti brojke iz pogrešnog pisma unutar teksta koji je trebao biti ujednačenog fonta

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 vlastitom cjevovodu

FormTraversalStatus svaki snimak operatora Do pretvara u mali dijagnostički izvještaj, a cjevovod koji ga zanemari odbacuje upravo informacije koje bi objasnile nepotpuno izdvajanje. ftsNotApplicable znači da uputa uopće nije bila razriješen poziv Form objekta; ftsNotRequested znači da je rekurzija isključena za taj poziv; ftsEnumerated znači da je Form uspješno parsiran i obrađen; ftsDepthLimit i ftsCycle označavaju dva načina na koja se ulazak namjerno prekida; a ftsMalformed obuhvaća sve ostalo što je zaustavilo obilazak — nerazrješivu referencu na tok, neuspješno parsiranje /Matrix ili /BBox ili iznimku podignutu tijekom izvršavanja vlastitog sadržaja Form objekta. Taj je posljednji slučaj važan u radu jer neuspjeli ugniježđeni obilazak vraća sve što je djelomično proizveo za tu granu, pa pozivatelj nikada ne mora nagađati je li Form uistinu bio prazan ili se srušio dvije upute nakon početka svojeg toka 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 mjesto u većem sustavu

Tok sadržaja Form objekta dekodira se i parsira točno jednom po pozivu enumeracije, bez obzira na to koliko se puta Form pozove, jer PDFlibPas predmemorira parsirani popis uputa uz temeljni objekt toka umjesto da ga ponovno parsira pri svakom susjednom pozivu — pečat s tri kuta iz uvodnog primjera dekodira se jednom i obrađuje tri puta, a ne dekodira tri puta. Ono što se pri svakom pojedinom pozivu ponovno izgrađuje jest sve što se legitimno razlikuje između jednog mjesta poziva i sljedećeg: dječji praćitelj, spojeni CTM, presjek isijecanja i doseg resursa. To praćenje CTM-a i isijecanja po pozivu ista je infrastruktura koja stoji iza praćenja CTM-a i stanja isijecanja toka sadržaja u PDFlibPas-u, pa ga vrijedi pročitati uz ovaj članak za svaki obilazak toka sadržaja koji nadilazi samu rekurziju Form objekata

Prije nego što ovaj API uključite u veći cjevovod, vrijedi postaviti očekivanja o dva ograničenja. Strop dubine od 64 razine nije regulator za legitimno duboke dokumente, jer se u stvarnim računima, izvodima i predlošcima izvještaja Form objekti gotovo nikada ne ugnijezde više od tri ili četiri razine — dokument koji dosegne ftsDepthLimit mnogo je vjerojatnije neispravan ili zlonamjeran nego neobično složen, pa ga vrijedi zapisati kao signal kvalitete podataka umjesto tiho ponoviti s većim brojem. EnumPageContentStatesEx također je API za analizu pri čitanju: prijavljuje što tok sadržaja radi, a ne treba li Form uopće biti vidljiv, što je zasebno pitanje na koje odgovara stanje vidljivosti neobavezne grupe sadržaja kada se pečat ili vodeni žig Form nalazi iza sloja koji je preglednik možda isključio. Otkrivanje ciklusa lancem poziva, izolacija po pozivu i ograničavanje dosega resursa zajedno čine jedan dio površine za pregled tokova sadržaja u komponenti PDFlibPas za Delphi i C++Builder

Autorska prava 2026 losLab Software

Početna · Pretraži · losLab.com