PDFlibPas løser rekursive Form XObject-kall i Delphi PDF-innholdsstrømmer ved å spore den aktive kallkjeden, ikke et globalt besøkt-sett, slik at TPDFlib.EnumPageContentStatesEx kan gå gjennom den samme Form-en påkalt flere ganger på én side uten å forveksle legitim gjenbruk med en syklus. En stempel-Form-XObject i en faktura-mal er det typiske tilfellet: det samme objektet kalles fra toppteksten, bunnteksten, og et vannmerke-lag på én side, og bare en kallkjede som løper tilbake på seg selv, er en genuin syklus
ISO 32000-1 §8.10 definerer en Form XObject som en selvstendig innholdsstrøm en side, eller en annen Form, påkaller med Do-operatoren, komplett med sitt eget koordinatsystem i /Matrix, en klippegrense i det koordinatsystemet i /BBox, og valgfritt sin egen ressursordbok. Ingenting i spesifikasjonen setter et tak på hvor mange ganger én Form kan påkalles eller hvor dypt Forms kan påkalle hverandre, så en konform parser må akseptere legitim gjenbruk og legitim nøsting mens den fortsatt forsvarer seg mot det ene arrangementet spesifikasjonen faktisk forbyr: en Form hvis innholdsstrøm, direkte eller transitivt, påkaller seg selv. PDFlibPas rapporterer det skillet gjennom TPDFlibContentFormTraversalStatus-verdiene festet til hvert Do-øyeblikksbilde, mest bemerkelsesverdig ftsEnumerated for en vellykket nedstigning og ftsCycle for det ene tilfellet som faktisk er en løkke
Hvorfor utløser ikke gjenbruk av den samme Form XObject en falsk syklus?
En gjentatt Form XObject-referanse er ikke, i seg selv, bevis på noe galt. ISO 32000-1 tillater at det samme Form-objektet påkalles fra så mange steder i en innholdsstrøm forfatteren vil, noe som er nøyaktig hvordan et logo-stempel, en brevhode-mal, eller en sidetall-bunntekst gjenbrukes over en side uten å duplisere innholdsstrømmen dens flere ganger. Den naive vakten mot løpsk rekursjon er ett enkelt besøkt-sett nøkkelsatt på objektnummer: første gang en gjennomgang ser Form-objekt 12, markerer den 12 som sett og nekter å gå inn i den igjen noe annet sted i treet. Den tilnærmingen brekker i det øyeblikket det samme stempelet dukker opp i to urelaterte hjørner av én side, fordi det andre, fullstendig legitime kallet ankommer etter at objektnummeret allerede er markert sett og avvises som om det var en løkke
PDFlibPas unngår den falske positive ved å avgrense syklusdeteksjon til den gjeldende kallkjeden i stedet for hele dokumentet. EnumPageContentStatesEx skyver den løste Form-strømmen inn på den aktive kallkjeden umiddelbart før den stiger ned i den, og popper deretter den samme oppføringen av igjen i det øyeblikket nedstigningen returnerer, vellykket eller ikke. En søster-påkalling av den identiske strømmen starter først etter at den første allerede er poppet, så kallkjeden er fri for den strømmen når søster-kallet sjekker den, og gjennomgangen enumererer den nøyaktig slik den ville gjort med enhver annen Form. En ekte syklus ser annerledes ut på den samme kjeden: Form A kaller Form B, B er fortsatt åpen på kjeden når sitt eget innhold kaller tilbake inn i A, og A sitter fortsatt på kjeden fra det ytre kallet som ikke har returnert ennå — det er den eneste formen ftsCycle rapporterer, en Form-strøm fortsatt åpen et sted tidligere på den gjeldende kallkjeden, ikke bare til stede et annet sted på siden
Hvor dypt kan Form XObject-rekursjon gå før PDFlibPas stopper den?
Syklusdeteksjon og dybdebegrensning løser to forskjellige problemer, og PDFlibPas holder dem som to forskjellige TPDFlibContentFormTraversalStatus-utfall nettopp av den grunnen. En kjede med tjue distinkte Forms, hver kaller den neste og ingen av dem gjentar seg, er ikke en syklus etter noen definisjon — den aktive-kjede-sjekken finner aldri en gjentatt strøm — men tjue ærlige nivåer av nøsting er fortsatt tjue nivåer av parsing, matrisekonkatenering, og ressursoppløsning som en feilformet eller fiendtlig PDF kunne presse vilkårlig høyere hvis ingenting annet stoppet den. EnumPageContentStatesEx tar en MaxFormDepth-parameter nettopp av denne grunnen og klemmer hvilken som helst verdi sendt til et maksimum på 64, uansett hva kalleren ber om. En dybde på null er et spesialtilfelle verdt å kjenne til på egen hånd: den deaktiverer Form-rekursjon fullstendig og reproduserer den flate, bare-side-oppførselen til den eldre EnumPageContentStates-metoden, noe som er grunnen til at hvert Do-øyeblikksbilde i den modusen rapporterer ftsNotRequested i stedet for å forsøke noe
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;
Én under-sporer per påkalling: å isolere grafikktilstand
Hver nedstigning inn i en Form XObject får sin egen grafikktilstands-sporer i stedet for å dele den som allerede går gjennom siden, fordi en Forms innholdsstrøm er påkrevd å etterlate grafikktilstanden nøyaktig slik den fant den, og PDFlibPas kan ikke anta at hver PDF den åpner, faktisk respekterer det kravet. Barne-sporeren starter fra et øyeblikksbilde av hvilken som helst CTM, fargetilstand, og tekstparametere som var aktive ved den kallende Do-instruksjonen, og tilbakestiller deretter sin egen lagre-og-gjenopprett-stakk og gjeldende-bane-sporing til tomt før den utfører en eneste instruksjon av Form-en. En ubalansert q uten noen matchende Q inne i en uforsiktig eller skadet Form, ikke en sjelden ting å finne i PDF-er produsert av eldre verktøy, forblir inneholdt inne i den ene påkallingens sporer og lekker aldri inn i side-sporeren eller inn i en søster-påkalling av det samme stempelet som sitter én linje senere i innholdsstrømmen
Form-/Matrix komponeres med CTM-en i kraft ved Do-en på samme måte som en cm-operator gjør, venstre-multiplisert mot den gjeldende transformasjonen snarere enn substituert for den, og PDFlibPas gjenbruker bevisst den ene kodeveien i stedet for å vedlikeholde en andre formel, ettersom to uavhengige implementasjoner av den samme matrisealgebraen er nøyaktig den typen duplisering som stille driver fra hverandre etter noen runder med skala-, roter-, og skjær-komposisjon. /BBox klipper deretter i Form-ens eget koordinatrom etter at matrisen allerede er anvendt, og alle fire hjørnene av den boksen transformeres individuelt i stedet for bare de motsatte hjørnene, ettersom en rotert eller skjært Form ellers kan rapportere en avgrensningsboks som bommer på ekte innhold som sitter i det som pleide å være et ekstremt hjørne før transformasjonen flyttet det et annet sted. Å utvide løkken fra det forrige eksempelet over det samme States-arrayet leser de feltene direkte
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);
Deler to Forms med samme ressursnavn én skrifttype?
Nei. Et ressursnavn slik som /F1 betyr bare noe relativt til ressursordboken aktiv på punktet det brukes, og to forskjellige Form XObjects står fritt til å definere to fullstendig forskjellige skrifttyper under det identiske navnet. PDFlibPas løser dette ved å spore et ressursomfang ved siden av hvert ressursnavn: når en Form bærer sin egen /Resources-ordbok, blir den ordboken det fullstendige ressursomfanget for alt inni den, uten noen per-nøkkel-reserveløsning til side- eller kaller-ordboken for hva Form-ens egen ordbok enn utelater. Bare en Form uten noen /Resources-nøkkel i det hele tatt, et mønster fortsatt produsert av noen eldre PDF-generatorer, arver den kallende ordboken i sin helhet, og det er et bevisst kompatibilitets-unntak snarere enn en generell regel verdt å lene seg på i ny utdata. Skrifttype-identitet i et TPDFlibContentGraphicsState-øyeblikksbilde er derfor paret av FontResource og FontResourceScope, ikke navnet alene, med FontObjectNumber tilgjengelig for å bekrefte nøyaktig hvilket indirekte objekt en gitt /F1 løste til i det bestemte omfanget
Det samme omfanget gjelder for hver annen navngitt ressurs en Form kan bære, ExtGState-oppføringer og nøstede XObject-oppføringer inkludert, ettersom den underliggende oppløsningsmekanismen ikke spesialbehandler skrifttyper — skrifttype-tilfellet betyr bare tilfeldigvis mest, fordi en feilmatchet skrifttype-identitet stille produserer feil glyfer i stedet for en åpenbar feil. Uttrekkingskode som grupperer tekstforløp etter skrifttypenavn alene, uten også å gruppere etter ressursomfang, vil slå sammen to visuelt forskjellige skrifttyper som tilfeldigvis deler et navn, og feilen vil ikke vise seg før noen legger merke til tall fra feil skrifttype som sitter inne i det som skulle lese som én konsistent skrifttype
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);
Å lese FormTraversalStatus i din egen pipeline
FormTraversalStatus gjør hvert Do-øyeblikksbilde til en liten diagnostisk rapport på egen hånd, og en pipeline som ignorerer den, kaster bort nøyaktig informasjonen som ville forklart en ufullstendig uttrekking. ftsNotApplicable betyr at instruksjonen aldri var en løst Form-påkalling i utgangspunktet; ftsNotRequested betyr at rekursjon var skrudd av for dette kallet; ftsEnumerated betyr at Form-en ble parset og gjennomgått vellykket; ftsDepthLimit og ftsCycle markerer de to måtene en nedstigning kuttes kort med hensikt; og ftsMalformed dekker alt annet som stoppet gjennomgangen — en uløselig strøm-referanse, en /Matrix eller /BBox som feilet å parse, eller et unntak kastet mens Form-ens eget innhold ble utført. Det siste tilfellet betyr noe operasjonelt, fordi en mislykket nøstet gjennomgang ruller tilbake hva som enn av delvis utdata den allerede hadde produsert for den grenen, slik at en kaller aldri trenger å gjette om en Form var genuint tom eller ganske enkelt eksploderte to instruksjoner inn i innholdsstrømmen dens
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;
Grenser, kostnader, og hvor dette passer inn
En Forms innholdsstrøm dekodes og parses nøyaktig én gang per enumereringskall uansett hvor mange ganger Form-en blir påkalt, fordi PDFlibPas bufrer den parsede instruksjonslisten mot den underliggende strømobjektet i stedet for å parse den på nytt ved hvert søster-kall — tre-hjørne-stempelet fra det innledende eksempelet dekodes én gang og gås gjennom tre ganger, ikke dekodes tre ganger. Hva som faktisk gjenoppbygges ved hver eneste påkalling, er alt som legitimt skiller seg mellom ett kallsted og det neste: barne-sporeren, den konkatenerte CTM-en, den skjærte klippingen, og ressursomfanget. Den per-påkallings-CTM- og klippe-regnskapsføringen er det samme maskineriet bak PDFlibPas' innholdsstrøm-CTM- og klippe-tilstandssporer, verdt å lese sammen med denne for enhver innholdsstrøm-gjennomgang som går utover selve Form-rekursjonen
To grenser er verdt å sette forventninger rundt før dette API-et går inn i en større pipeline. 64-nivå-dybdetaket er ikke en finjusteringsknapp for legitimt dype dokumenter, ettersom ekte fakturaer, kontoutskrifter, og rapportmaler i det vesentlige aldri nøster Forms mer enn tre eller fire nivåer dypt — et dokument som faktisk treffer ftsDepthLimit, er langt mer sannsynlig å være feilformet eller fiendtlig enn uvanlig utførlig, og er verdt å logge som et data-kvalitets-signal snarere enn å stille prøves på nytt med et større tall. EnumPageContentStatesEx er også et lese-side-analyse-API: det rapporterer hva en innholdsstrøm gjør, ikke om en Form skal være synlig i det hele tatt, noe som er et separat spørsmål besvart av Optional Content Group-synlighetstilstand når et stempel eller vannmerke-Form sitter bak et lag en fremviser kan ha slått av. Kallkjede-syklusdeteksjon, per-påkalling-isolasjon, og ressursomfang utgjør sammen ett hjørne av innholdsstrøm-inspeksjonsoverflaten i PDFlibPas-komponenten for Delphi og C++Builder