PDFlibPas sprendžia rekursinius Form XObject iškvietimus Delphi PDF turinio srautuose, sekdamas aktyvią iškvietimų grandinę, o ne visuotinį aplankytų objektų rinkinį, todėl TPDFlib.EnumPageContentStatesEx gali pereiti per tą pačią Form, iškviestą kelis kartus viename puslapyje, nepalaikydamas teisėto pakartotinio naudojimo ciklu. Sąskaitos faktūros šablono antspaudo Form XObject yra tipiškas atvejis: tas pats objektas viename puslapyje iškviečiamas antraštėje, poraštėje ir vandens ženklo sluoksnyje, o tik į save grįžtanti iškvietimų grandinė yra tikras ciklas
ISO 32000-1 §8.10 apibrėžia Form XObject kaip savarankišką turinio srautą, kurį puslapis arba kita Form iškviečia operatoriumi Do, kartu su atskira koordinačių sistema faile /Matrix, apkarpymo riba toje koordinačių sistemoje faile /BBox ir, pasirinktinai, atskiru išteklių žodynu. Standartas neriboja, kiek kartų galima iškviesti vieną Form arba kaip giliai Form gali iškviesti viena kitą, todėl atitinkantis analizatorius turi priimti teisėtą pakartotinį naudojimą ir teisėtą įdėjimą, kartu apsisaugodamas nuo vienintelės standartui prieštaraujančios struktūros: Form, kurios turinio srautas tiesiogiai arba per kitus objektus iškviečia pačią save. PDFlibPas šį skirtumą pateikia per TPDFlibContentFormTraversalStatus reikšmes, priskirtas kiekvienai Do momentinei būsenai, ypač ftsEnumerated sėkmingam nusileidimui ir ftsCycle vieninteliam atvejui, kuris iš tikrųjų yra ciklas
Kodėl pakartotinai naudojant tą pačią Form XObject nesukuriamas klaidingas ciklas?
Pakartotinė Form XObject nuoroda pati savaime nereiškia, kad kažkas negerai. ISO 32000-1 leidžia tą patį Form objektą iškviesti iš tiek turinio srauto vietų, kiek nori autorius, ir būtent taip logotipo antspaudas, firminio blanko šablonas ar puslapio numerio poraštė pakartotinai naudojami visame puslapyje nedubliuojant jo turinio srauto kelis kartus. Naivus apsaugos nuo nekontroliuojamos rekursijos būdas yra vienas aplankytų objektų rinkinys, indeksuojamas objekto numeriu: pirmą kartą pamatęs Form objektą 12, pereiklys pažymi 12 kaip matytą ir atsisako į jį įeiti bet kur kitur medyje. Šis būdas sugenda, kai tas pats antspaudas pasirodo dviejuose nesusijusiuose puslapio kampuose, nes antras visiškai teisėtas iškvietimas aptinka jau pažymėtą objekto numerį ir atmetamas taip, tarsi būtų ciklas
PDFlibPas išvengia šio klaidingo teigiamo rezultato apribodamas ciklų aptikimą dabartine iškvietimų grandine, o ne visu dokumentu. EnumPageContentStatesEx išspręstą Form srautą į aktyvią iškvietimų grandinę įdeda prieš pat nusileidimą į jį, o nusileidimui grįžus, nesvarbu, sėkmingai ar ne, tą pačią grandies dalį pašalina. Toks pats srautas gretimame iškvietime pradedamas tik tada, kai pirmasis jau pašalintas, todėl grandinėje šio srauto nebelieka ir gretimas iškvietimas jį tikrina kaip naują Form, o pereiklis jį išvardija taip pat, kaip bet kurią kitą Form. Tikras ciklas toje pačioje grandinėje atrodo kitaip: Form A iškviečia Form B, B dar neatverta grandinėje, kai jos turinys vėl iškviečia A, o A vis dar yra grandinėje dėl negrįžusio išorinio iškvietimo — tai vienintelė forma, kurią praneša ftsCycle, tai yra anksčiau dabartinėje iškvietimų grandinėje dar neatverta Form, o ne tiesiog kitur puslapyje esanti Form
Kaip giliai gali nusileisti Form XObject rekursija, kol PDFlibPas ją sustabdo?
Ciklų aptikimas ir gylio ribojimas sprendžia dvi skirtingas problemas, todėl PDFlibPas jas pateikia kaip dvi skirtingas TPDFlibContentFormTraversalStatus baigtis. Dvidešimties skirtingų Form grandinė, kur kiekviena iškviečia kitą ir nė viena nepasikartoja, pagal jokį apibrėžimą nėra ciklas — aktyvios grandinės patikra neranda pasikartojančio srauto — tačiau dvidešimt sąžiningų įdėjimo lygių vis tiek reiškia dvidešimt analizės, matricos sujungimo ir išteklių sprendimo lygių, kuriuos sugadintas ar priešiškas PDF galėtų be galo didinti, jei niekas jo nesustabdytų. EnumPageContentStatesEx būtent dėl šios priežasties priima MaxFormDepth parametrą ir bet kokią perduotą reikšmę apriboja iki 64, nepaisydamas to, ko prašo iškvietėjas. Nulinis gylis yra atskiras svarbus atvejis: jis visiškai išjungia Form rekursiją ir atkuria ankstesnio EnumPageContentStates metodo plokščią, tik puslapio lygio veikimą, todėl kiekviena tos veiksenos Do momentinė būsena praneša ftsNotRequested, užuot ką nors bandžiusi
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;
Vienas antrinis sekiklis kiekvienam iškvietimui: grafikos būsenos izoliavimas
Kiekvienas nusileidimas į Form XObject gauna atskirą grafikos būsenos sekiklį, o ne dalijasi tuo, kuris jau eina per puslapį, nes Form turinio srautas privalo palikti grafikos būseną tiksliai tokią, kokią rado, o PDFlibPas negali manyti, kad kiekvienas atidaromas PDF iš tikrųjų laikosi šio reikalavimo. Antrinis sekiklis pradedamas nuo momentinės būsenos su tuo CTM, spalvų būsena ir teksto parametrais, kurie buvo aktyvūs iškviečiančioje Do instrukcijoje, tada jo paties išsaugojimo ir atkūrimo dėklas bei dabartinio kelio sekimas iš naujo nustatomi į tuščius prieš vykdant vieną Form instrukciją. Nesubalansuotas q, neturintis atitinkančio Q neatsargioje ar sugadintoje Form, o tai nėra reta senesnių įrankių sukurtiems PDF, lieka izoliuotas tame viename iškvietimo sekiklyje ir niekada nepatenka į puslapio sekiklį ar į gretimą to paties antspaudo iškvietimą, esantį kita eilute turinio sraute
Form /Matrix sujungiamas su CTM, veikiančiu Do metu, taip pat kaip operatorius cm, kairėje dauginamas iš dabartinės transformacijos, o ne ją pakeičia, ir PDFlibPas sąmoningai pakartotinai naudoja tą patį kodo kelią, užuot prižiūrėjęs antrą formulę, nes dvi nepriklausomos tos pačios matricos algebros realizacijos yra būtent toks dubliavimas, kuris po kelių mastelio, pasukimo ir poslinkio kompozicijos etapų tyliai išsiskiria. Tada /BBox apkerpa Formos koordinačių erdvėje, kai matrica jau pritaikyta, o visi keturi šio langelio kampai transformuojami atskirai, o ne tik priešingi kampai, nes pasukta ar poslinkiu deformuota Form kitaip gali pranešti ribas, praleidžiančias tikrą turinį, kuris prieš transformaciją buvo kraštiniame kampe, o po jos atsidūrė kitur. Išplėtus ankstesnio pavyzdžio ciklą per tą patį States masyvą, šie laukai nuskaitomi tiesiogiai
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);
Ar dvi Form su tuo pačiu išteklių vardu dalijasi vienu šriftu?
Ne. Toks išteklių vardas kaip /F1 ką nors reiškia tik atsižvelgiant į tuo metu aktyvų išteklių žodyną, o du skirtingi Form XObject objektai gali laisvai apibrėžti du visiškai skirtingus šriftus tuo pačiu pavadinimu. PDFlibPas tai išsprendžia sekdamas išteklių sritį šalia kiekvieno išteklių vardo: kai Form turi savo /Resources žodyną, šis žodynas tampa visa išteklių sritimi viskam, kas yra jo viduje, ir nėra jokio atskiro rakto atsarginio grįžimo į puslapio ar iškvietėjo žodyną tam, ko Formos žodyne nėra. Tik Form, kuri apskritai neturi rakto /Resources, o tokį modelį vis dar sukuria kai kurie senesni PDF generatoriai, paveldi visą iškviečiantį žodyną, ir tai yra sąmoninga suderinamumo išimtis, o ne bendra taisyklė, kuria verta remtis naujame išvesties faile. Todėl šrifto tapatybė TPDFlibContentGraphicsState momentinėje būsenoje yra FontResource ir FontResourceScope pora, o ne vien vardas, be to, FontObjectNumber leidžia patvirtinti, į kurį konkretų netiesioginį objektą konkretus /F1 išspręstas toje srityje
Tas pats aprėpties principas taikomas kiekvienam kitam pavadintam ištekliui, kurį gali turėti Form, įskaitant ExtGState įrašus ir įdėtus XObject įrašus, nes pagrindinis sprendimo mechanizmas neišskiria šriftų — šriftų atvejis tiesiog svarbiausias, kadangi neatitinkanti šrifto tapatybė tyliai sukuria neteisingus glifus, o ne akivaizdžią klaidą. Ištraukimo kodas, grupuojantis teksto sekas vien pagal šrifto vardą ir negrupuojantis pagal išteklių sritį, sujungs du vizualiai skirtingus šriftus, kurie atsitiktinai dalijasi vardu, o klaida išryškės tik tada, kai kas nors pastebės netinkamo šrifto skaitmenis ten, kur turėjo būti vientisas tekstas
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);
FormTraversalStatus skaitymas savo apdorojimo grandinėje
FormTraversalStatus kiekvieną Do momentinę būseną paverčia nedidele savarankiška diagnostikos ataskaita, o grandinė, kuri ją ignoruoja, išmeta būtent tą informaciją, kuri paaiškintų neišsamų ištraukimą. ftsNotApplicable reiškia, kad instrukcija iš pradžių nebuvo išspręstas Form iškvietimas; ftsNotRequested reiškia, kad šio iškvietimo rekursija buvo išjungta; ftsEnumerated reiškia, kad Form buvo išanalizuota ir sėkmingai pereita; ftsDepthLimit ir ftsCycle žymi du atvejus, kai nusileidimas tyčia nutraukiamas; o ftsMalformed apima visa kita, kas sustabdė ėjimą — neišsprendžiamą srauto nuorodą, neanalizuojamą /Matrix ar /BBox arba išimtį, iškeltą vykdant pačios Form turinį. Pastarasis atvejis svarbus veikimo požiūriu, nes nepavykęs įdėtas ėjimas grąžina visą dalinę išvestį, kurią jau buvo sukūręs tai šakai, todėl iškvietėjui nereikia spėlioti, ar Form buvo iš tikrųjų tuščia, ar tiesiog sugriuvo po dviejų turinio instrukcijų
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;
Ribos, sąnaudos ir vieta bendrame sprendime
Form turinio srautas kiekvieno išvardijimo iškvietimo metu dekoduojamas ir analizuojamas tik vieną kartą, nesvarbu, kiek kartų Form iškviečiama, kadangi PDFlibPas išanalizuotų instrukcijų sąrašą saugo prie pagrindinio srauto objekto, o ne iš naujo analizuoja per kiekvieną gretimą iškvietimą — atidarymo pavyzdžio trijų kampų antspaudas dekoduojamas vieną kartą ir pereinamas tris kartus, o ne dekoduojamas tris kartus. Kiekvieno iškvietimo metu iš naujo sukuriama tai, kas teisėtai skiriasi tarp vienos iškvietimo vietos ir kitos: antrinis sekiklis, sujungtas CTM, susikirtęs apkarpymas ir išteklių sritis. Šis kiekvienam iškvietimui tenkantis CTM ir apkarpymo apskaitos mechanizmas yra tas pats, kuris naudojamas PDFlibPas turinio srauto CTM ir apkarpymo būsenos sekiklyje, todėl jį verta perskaityti kartu su šiuo straipsniu analizuojant bet kokį turinio srautą, kuris peržengia vien Form rekursijos ribas
Prieš įtraukiant šią API į didesnę apdorojimo grandinę verta nusistatyti lūkesčius dėl dviejų ribų. 64 lygių gylio riba nėra teisėtų gilių dokumentų derinimo rankenėlė, nes tikros sąskaitos, išrašai ir ataskaitų šablonai beveik niekada neįdeda Form daugiau nei trimis ar keturiais lygiais — dokumentas, pasiekiantis ftsDepthLimit, daug dažniau yra sugadintas ar priešiškas, o ne neįprastai sudėtingas, todėl jį verta registruoti kaip duomenų kokybės signalą, o ne tyliai kartoti su didesniu skaičiumi. EnumPageContentStatesEx taip pat yra tik skaitymo analizės API: ji praneša, ką daro turinio srautas, bet ne tai, ar Form apskritai turi būti matoma, nes tai atskiras klausimas, į kurį atsako pasirenkamos turinio grupės matomumo būsena, kai antspaudo ar vandens ženklo Form yra po sluoksniu, kurį peržiūros programa galėjo išjungti. Iškvietimų grandinės ciklų aptikimas, izoliavimas kiekvienam iškvietimui ir išteklių aprėptis kartu sudaro vieną turinio srauto tikrinimo paviršiaus dalį PDFlibPas komponente, skirtoje Delphi ir C++Builder