PDFlibPas ratkaisee rekursiiviset Form XObject -kutsut Delphi-PDF-sisältövirroissa seuraamalla aktiivista kutsuketjua, ei globaalia vierailtu-joukkoa, joten TPDFlib.EnumPageContentStatesEx voi kävellä saman lomakkeen läpi, jota kutsutaan useita kertoja yhdellä sivulla, sekoittamatta laillista uudelleenkäyttöä sykliksi. Leimalomake Form XObject laskutusmallissa on tyypillinen tapaus: samaa oliota kutsutaan otsikosta, alatunnisteesta ja vesileimakerroksesta yhdellä sivulla, ja vain kutsuketju, joka silmukoituu takaisin itseensä, on aito sykli
ISO 32000-1 §8.10 määrittelee Form XObjectin itsenäisenä sisältövirtana, jota sivu, tai toinen lomake, kutsuu Do-operaattorilla, täydellisenä omalla koordinaattijärjestelmällään kentässä /Matrix, rajausrajalla tuossa koordinaattijärjestelmässä kentässä /BBox, ja valinnaisesti omalla resurssisanakirjallaan. Mikään spesifikaatiossa ei rajaa, kuinka monta kertaa yhtä lomaketta voidaan kutsua tai kuinka syvälle lomakkeet voivat kutsua toisiaan, joten standardinmukaisen jäsentimen on hyväksyttävä laillinen uudelleenkäyttö ja laillinen sisäkkäisyys samalla, kun se puolustautuu sitä yhtä järjestelyä vastaan, jonka spesifikaatio todella kieltää: lomake, jonka sisältövirta, suoraan tai transitiivisesti, kutsuu itseään. PDFlibPas raportoi tuon eron TPDFlibContentFormTraversalStatus-arvojen kautta, jotka on liitetty jokaiseen Do-tilannekuvaan, huomattavimpina ftsEnumerated onnistuneelle laskeutumiselle ja ftsCycle sille yhdelle tapaukselle, joka todella on silmukka
Miksi saman Form XObjectin uudelleenkäyttö ei laukaise väärää sykliä?
Toistuva Form XObject -viittaus ei itsessään ole todiste mistään väärästä. ISO 32000-1 sallii saman lomake-olion kutsuttavan niin monesta paikasta sisältövirrassa kuin tekijä haluaa, mikä on juuri se, miten logoleima, kirjelomakemalli tai sivunumeroalatunniste uudelleenkäytetään sivun yli monistamatta sen sisältövirtaa useaan kertaan. Naiivi suoja karkaavaa rekursiota vastaan on yksi vierailtu-joukko avaimena olionumero: ensimmäisellä kerralla, kun kävelijä näkee lomake-olion 12, se merkitsee 12:n nähdyksi ja kieltäytyy astumasta siihen uudelleen missään muualla puussa. Tuo lähestymistapa hajoaa heti, kun sama leima ilmestyy kahdessa toisiinsa liittymättömässä kulmassa yhdellä sivulla, koska toinen, täysin laillinen kutsu saapuu sen jälkeen, kun olionumero on jo merkitty nähdyksi, ja se hylätään ikään kuin se olisi silmukka
PDFlibPas välttää tuon väärän positiivisen rajaamalla syklintunnistuksen nykyiseen kutsuketjuun koko asiakirjan sijaan. EnumPageContentStatesEx työntää ratkaistun lomakevirran aktiiviseen kutsuketjuun välittömästi ennen siihen laskeutumista, ja poimii sitten saman merkinnän pois heti, kun laskeutuminen palautuu, onnistuneesti tai ei. Identtisen virran sisaruskutsu alkaa vasta sen jälkeen, kun ensimmäinen on jo poimittu pois, joten kutsuketju on selvä tuosta virrasta siihen mennessä, kun sisaruskutsu tarkistaa sen, ja kävelijä luetteloi sen täsmälleen kuten se luetteloisi minkä tahansa muun lomakkeen. Todellinen sykli näyttää erilaiselta samalla ketjulla: Lomake A kutsuu Lomaketta B, B on yhä auki ketjussa, kun sen oma sisältö kutsuu takaisin A:han, ja A on yhä ketjussa ulomasta kutsusta, joka ei ole vielä palautunut — se on ainoa muoto, jonka ftsCycle raportoi, lomakevirta, joka on yhä auki jossain aiemmin nykyisessä kutsuketjussa, ei vain läsnä jossain muualla sivulla
Kuinka syvälle Form XObject -rekursio voi mennä, ennen kuin PDFlibPas pysäyttää sen?
Syklintunnistus ja syvyysrajoitus ratkaisevat kaksi eri ongelmaa, ja PDFlibPas pitää ne kahtena eri TPDFlibContentFormTraversalStatus-lopputuloksena juuri tästä syystä. Ketju kahtakymmentä erillistä lomaketta, joista jokainen kutsuu seuraavaa eikä yksikään toistu, ei ole sykli millään määritelmällä — aktiivisen ketjun tarkistus ei koskaan löydä toistuvaa virtaa — mutta kaksikymmentä rehellistä sisäkkäisyystasoa on silti kaksikymmentä tasoa jäsentämistä, matriisin yhdistämistä ja resurssin ratkaisemista, jota virheellinen tai vihamielinen PDF voisi työntää mielivaltaisesti korkeammalle, jos mikään muu ei pysäyttäisi sitä. EnumPageContentStatesEx ottaa MaxFormDepth-parametrin juuri tästä syystä ja rajaa mitä tahansa välitettyä arvoa enintään 64:ään, riippumatta siitä, mitä kutsuja pyytää. Syvyys nolla on erikoistapaus, joka kannattaa tuntea erikseen: se poistaa lomakerekursion kokonaan käytöstä ja toistaa vanhemman EnumPageContentStates-metodin tasaisen, vain-sivu-käytöksen, minkä vuoksi jokainen Do-tilannekuva tuossa tilassa raportoi ftsNotRequested-arvon yrittämättä mitään
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;
Yksi ali-seurantalaite per kutsu: graafisen tilan eristäminen
Jokainen laskeutuminen Form XObjectiin saa oman graafisen tilan seurantalaitteensa sen sijaan, että se jakaisi sivua jo kävelevän seurantalaitteen, koska lomakkeen sisältövirran vaaditaan jättävän graafinen tila täsmälleen sellaisena kuin se sen löysi, eikä PDFlibPas voi olettaa jokaisen avaamansa PDF:n todella kunnioittavan tuota vaatimusta. Lapsiseurantalaite alkaa tilannekuvasta siitä, mikä CTM, väritila ja tekstiparametrit olivat aktiivisia kutsuvassa Do-ohjeessa, ja nollaa sitten oman tallenna-ja-palauta-pinonsa ja nykyisen polun seurannan tyhjäksi ennen kuin se suorittaa yhtäkään lomakkeen ohjetta. Epätasapainoinen q ilman vastaavaa Q-merkintää huolimattomassa tai vioittuneessa lomakkeessa, ei harvinainen löydös vanhempien työkalujen tuottamissa PDF-tiedostoissa, pysyy sisällytettynä tuon yhden kutsun seurantalaitteen sisään eikä koskaan vuoda sivun seurantalaitteeseen tai saman leiman sisaruskutsuun, joka istuu yhden rivin päässä sisältövirrassa
Lomakkeen /Matrix yhdistyy voimassa olevan CTM:n kanssa Do:ssa samalla tavalla kuin cm-operaattori tekee, vasemmalta kerrottuna nykyistä muunnosta vasten sen korvaamisen sijaan, ja PDFlibPas tarkoituksella käyttää uudelleen tuota yhtä koodipolkua sen sijaan, että ylläpitäisi toista kaavaa, koska kaksi riippumatonta toteutusta samasta matriisialgebrasta on juuri sellaista päällekkäisyyttä, joka hiljaa ajautuu erilleen muutaman skaalauksen, kierron ja leikkauksen yhdistämiskierroksen jälkeen. /BBox rajaa sitten lomakkeen omassa koordinaattitilassa sen jälkeen, kun matriisia on jo sovellettu, ja kaikki neljä tuon laatikon kulmaa muunnetaan yksitellen eikä vain vastakkaisia kulmia, koska kierretty tai leikattu lomake voi muuten raportoida rajauslaatikon, joka menettää todellista sisältöä, joka istuu siinä, mikä oli äärimmäinen kulma ennen kuin muunnos siirsi sen jonnekin muualle. Edellisen esimerkin silmukan laajentaminen saman States-taulukon yli lukee nuo kentät suoraan
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);
Jakavatko kaksi samannimistä resurssia kantavaa lomaketta yhden fontin?
Ei. Resurssinimi kuten /F1 tarkoittaa jotain vain suhteessa siihen resurssisanakirjaan, joka on aktiivinen sen käyttökohdassa, ja kahdella eri Form XObjectilla on vapaus määritellä kaksi täysin erilaista fonttia tuon identtisen nimen alla. PDFlibPas ratkaisee tämän seuraamalla resurssin laajuutta jokaisen resurssinimen rinnalla: kun lomake kantaa oman /Resources-sanakirjansa, tuosta sanakirjasta tulee koko resurssin laajuus kaikelle sen sisällä, ilman avainkohtaista varapalautusta sivun tai kutsujan sanakirjaan mille tahansa, mitä lomakkeen oma sanakirja sattuu jättämään pois. Vain lomake, jolla ei ole lainkaan /Resources-avainta, malli, jota jotkin vanhemmat PDF-generaattorit yhä tuottavat, perii kutsuvan sanakirjan kokonaan, ja se on tarkoituksellinen yhteensopivuuspoikkeus eikä yleinen sääntö, johon kannattaisi nojautua uudessa tulosteessa. Fonttien identiteetti TPDFlibContentGraphicsState-tilannekuvassa on siksi FontResource- ja FontResourceScope-pari, ei nimi yksinään, kun FontObjectNumber on saatavilla vahvistaakseen tarkalleen, mihin epäsuoraan olioon tietty /F1 ratkesi juuri tuossa laajuudessa
Sama laajuus koskee jokaista muuta nimettyä resurssia, jota lomake voi kantaa, ExtGState-merkinnät ja sisäkkäiset XObject-merkinnät mukaan lukien, koska taustalla oleva ratkaisumekanismi ei erityiskäsittele fontteja — fonttitapaus vain sattuu olemaan merkityksellisin, koska täsmäämätön fonttien identiteetti tuottaa hiljaa väärät glyfit ilmeisen epäonnistumisen sijaan. Poimintakoodi, joka ryhmittelee tekstiajot vain fonttinimen perusteella, ryhmittelemättä myös resurssin laajuuden mukaan, yhdistää kaksi visuaalisesti erilaista fonttia, jotka sattuvat jakamaan nimen, eikä virhe paljastu, ennen kuin joku huomaa numeroita väärästä kirjasintyypistä sen sisällä, minkä piti lukea yhtenä johdonmukaisena fonttina
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);
FormTraversalStatuksen lukeminen omassa putkessasi
FormTraversalStatus muuttaa jokaisen Do-tilannekuvan itsessään pieneksi diagnostiseksi raportiksi, ja putki, joka jättää sen huomiotta, heittää pois juuri sen tiedon, joka selittäisi vaillinaisen poiminnan. ftsNotApplicable tarkoittaa, ettei ohje koskaan ollutkaan ratkaistu lomakekutsu; ftsNotRequested tarkoittaa, että rekursio oli kytketty pois päältä tälle kutsulle; ftsEnumerated tarkoittaa, että lomake jäsennettiin ja käveltiin onnistuneesti; ftsDepthLimit ja ftsCycle merkitsevät kaksi tapaa, joilla laskeutuminen katkaistaan tarkoituksella; ja ftsMalformed kattaa kaiken muun, mikä pysäytti kävelyn — ratkaisematon virtaviittaus, /Matrix tai /BBox, joka ei jäsentynyt, tai poikkeus, joka nostettiin lomakkeen oman sisällön suorittamisen aikana. Tuo viimeinen tapaus on operatiivisesti merkityksellinen, koska epäonnistunut sisäkkäinen kävely perruu mitä tahansa osittaista tulostetta, jonka se oli jo tuottanut tuolle haaralle, joten kutsujan ei koskaan tarvitse arvata, oliko lomake aidosti tyhjä vai räjähtikö se yksinkertaisesti kahden ohjeen kohdalla sisältövirrassaan
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;
Rajat, kustannukset ja mihin tämä sopii
Lomakkeen sisältövirta puretaan ja jäsennetään täsmälleen kerran per luettelointikutsu riippumatta siitä, kuinka monta kertaa lomaketta kutsutaan, koska PDFlibPas välimuistittaa jäsennetyn ohjelistan taustalla olevaa virtaoliota vasten sen sijaan, että jäsentäisi sen uudelleen jokaisella sisaruskutsulla — avauksen kolmen kulman leima puretaan kerran ja käydään läpi kolme kertaa, ei pureta kolme kertaa. Se, mikä todella rakennetaan uudelleen jokaisella yksittäisellä kutsulla, on kaikki, mikä laillisesti eroaa yhden kutsupaikan ja seuraavan välillä: lapsiseurantalaite, yhdistetty CTM, leikattu rajaus ja resurssin laajuus. Tuo per-kutsu CTM- ja rajauskirjanpito on samaa koneistoa PDFlibPas:n sisältövirran CTM- ja rajaustilan seurantalaitteen takana, kannattaa lukea tämän rinnalla mille tahansa sisältövirran kävelylle, joka menee lomakerekursion itsensä ohitse
Kaksi rajaa kannattaa asettaa odotukset ympärille ennen kuin tämä API menee suurempaan putkeen. 64-tason syvyyskatto ei ole viritysvipu laillisesti syville asiakirjoille, koska todelliset laskut, tiliotteet ja raporttimallit käytännössä eivät koskaan sisäkkäistä lomakkeita enempää kuin kolme tai neljä tasoa — asiakirja, joka todella osuu ftsDepthLimit-arvoon, on paljon todennäköisemmin virheellinen tai vihamielinen kuin epätavallisen monimutkainen, ja kannattaa kirjata tietolaatusignaalina eikä hiljaa yrittää uudelleen suuremmalla luvulla. EnumPageContentStatesEx on myös lukupuolen analyysi-API: se raportoi, mitä sisältövirta tekee, ei sitä, pitäisikö lomakkeen olla lainkaan näkyvä, mikä on erillinen kysymys, johon vastaa Optional Content Groupin näkyvyystila, kun leima- tai vesileimalomake istuu kerroksen takana, jonka katseluohjelma on saattanut kytkeä pois päältä. Kutsuketjun syklintunnistus, per-kutsu-eristys ja resurssin laajuus muodostavat yhdessä yhden nurkan sisältövirran tarkastuspinnasta PDFlibPas-komponentissa Delphille ja C++Builderille