PDFlibPas lost recursieve Form-XObject-aanroepen in Delphi-PDF-inhoudsstromen op door de actieve aanroepketen bij te houden, niet een globale bezochte-verzameling, zodat TPDFlib.EnumPageContentStatesEx dezelfde Form die meerdere keren op één pagina wordt aangeroepen kan doorlopen zonder legitiem hergebruik te verwarren met een cyclus. Een stempel-Form-XObject in een factuursjabloon is het typische geval: hetzelfde object wordt aangeroepen vanuit de kopregel, de voetregel, en een watermerklaag op één pagina, en alleen een aanroepketen die op zichzelf terugloopt is een echte cyclus
ISO 32000-1 §8.10 definieert een Form-XObject als een zelfstandige inhoudsstroom die een pagina, of een andere Form, aanroept met de Do-operator, compleet met zijn eigen coördinatensysteem in /Matrix, een clipgrens in dat coördinatensysteem in /BBox, en optioneel zijn eigen resourcedictionary. Niets in de specificatie beperkt hoe vaak één Form aangeroepen kan worden of hoe diep Forms elkaar kunnen aanroepen, dus een conforme parser moet legitiem hergebruik en legitieme nesting accepteren terwijl hij zich toch verdedigt tegen de ene opzet die de specificatie wél verbiedt: een Form waarvan de inhoudsstroom, direct of transitief, zichzelf aanroept. PDFlibPas rapporteert dat onderscheid via de waarden TPDFlibContentFormTraversalStatus die aan elke Do-momentopname zijn gehecht, met name ftsEnumerated voor een geslaagde afdaling en ftsCycle voor het ene geval dat werkelijk een lus is
Waarom triggert het hergebruik van dezelfde Form-XObject geen valse cyclus?
Een herhaalde Form-XObject-verwijzing is op zichzelf geen bewijs van iets fout. ISO 32000-1 staat toe dat hetzelfde Form-object wordt aangeroepen vanuit zoveel plekken in een inhoudsstroom als de auteur wil, en dat is precies hoe een logostempel, een briefhoofdsjabloon, of een paginanummervoetregel over een pagina wordt hergebruikt zonder de inhoudsstroom ervan meerdere keren te dupliceren. De naïeve bewaking tegen op hol geslagen recursie is één enkele bezochte-verzameling gesleuteld op objectnummer: de eerste keer dat een doorloper Form-object 12 ziet, markeert het 12 als gezien en weigert het ergens anders in de boom er nogmaals in te gaan. Die aanpak breekt op het moment dat hetzelfde stempel op twee ongerelateerde hoeken van één pagina verschijnt, omdat de tweede, volledig legitieme aanroep binnenkomt nadat het objectnummer al als gezien is gemarkeerd en als een lus wordt afgewezen
PDFlibPas vermijdt dat vals-positieve door cyclusdetectie te scopen tot de huidige aanroepketen in plaats van het hele document. EnumPageContentStatesEx duwt de opgeloste Form-stream op de actieve aanroepketen direct voordat het erin afdaalt, en haalt datzelfde item er weer af zodra de afdaling terugkeert, succesvol of niet. Een gelijkwaardige aanroep van dezelfde stream vanuit een broer/zus begint pas nadat de eerste al is verwijderd, dus de aanroepketen is vrij van die stream tegen de tijd dat de broer/zus-aanroep deze controleert, en de doorloper somt hem gewoon op zoals elke andere Form. Een echte cyclus ziet er anders uit op diezelfde keten: Form A roept Form B aan, B staat nog open op de keten wanneer zijn eigen inhoud terugroept naar A, en A zit nog steeds op de keten vanuit de buitenste aanroep die nog niet is teruggekeerd — dat is de enige vorm die ftsCycle rapporteert, een Form-stream die nog ergens eerder op de huidige aanroepketen open staat, niet louter ergens anders op de pagina aanwezig
Hoe diep kan Form-XObject-recursie gaan voordat PDFlibPas het stopt?
Cyclusdetectie en dieptelimitering lossen twee verschillende problemen op, en PDFlibPas houdt ze precies daarom als twee verschillende TPDFlibContentFormTraversalStatus-uitkomsten. Een keten van twintig verschillende Forms, elk die de volgende aanroept en geen ervan herhaalt, is bij geen enkele definitie een cyclus — de actieve-keten-controle vindt nooit een herhaalde stream — maar twintig eerlijke nestniveaus zijn nog steeds twintig niveaus van parsen, matrixsamenstelling, en resourceoplossing die een misvormde of vijandige PDF willekeurig hoger zou kunnen duwen als niets anders het tegenhield. EnumPageContentStatesEx neemt precies om deze reden een MaxFormDepth-parameter en klemt welke waarde ook wordt doorgegeven vast op een maximum van 64, ongeacht wat de aanroeper vraagt. Een diepte van nul is een speciaal geval dat het waard is om apart te kennen: het schakelt Form-recursie helemaal uit en reproduceert het platte, alleen-pagina-gedrag van de oudere methode EnumPageContentStates, en dat is waarom elke Do-momentopname in die modus ftsNotRequested rapporteert in plaats van iets te proberen
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;
Eén subtracker per aanroep: grafische toestand isoleren
Elke afdaling in een Form-XObject krijgt zijn eigen grafische-toestand-tracker in plaats van er eentje te delen met degene die al over de pagina loopt, omdat van de inhoudsstroom van een Form wordt vereist dat deze de grafische toestand precies zo achterlaat als hij deze aantrof, en PDFlibPas kan niet aannemen dat elke PDF die het opent die vereiste ook daadwerkelijk naleeft. De onderliggende tracker begint vanuit een momentopname van welke CTM, kleurtoestand, en tekstparameters ook actief waren bij de aanroepende Do-instructie, en reset vervolgens zijn eigen save-and-restore-stack en huidige-pad-tracking naar leeg voordat het één enkele instructie van de Form uitvoert. Een onevenwichtige q zonder bijbehorende Q binnen een onzorgvuldige of beschadigde Form, niet zeldzaam om aan te treffen in PDF's geproduceerd door oudere tooling, blijft opgesloten binnen de tracker van die ene aanroep en lekt nooit naar de paginatracker of naar een gelijkwaardige aanroep van hetzelfde stempel die een regel later in de inhoudsstroom staat
Form-/Matrix stelt zich samen met de CTM die van kracht is bij de Do op dezelfde manier als een cm-operator, links-vermenigvuldigd tegen de huidige transformatie in plaats van deze te vervangen, en PDFlibPas hergebruikt doelbewust dat ene codepad in plaats van een tweede formule te onderhouden, aangezien twee onafhankelijke implementaties van dezelfde matrixalgebra precies het soort duplicatie is dat na een paar rondes schalen, roteren, en scheren stilzwijgend uit elkaar drijft. /BBox clipt vervolgens in de eigen coördinatenruimte van de Form nadat de matrix al is toegepast, en alle vier de hoeken van dat vak worden individueel getransformeerd in plaats van alleen de tegenoverliggende hoeken, aangezien een gedraaide of geschoorde Form anders een begrenzingskader kan rapporteren dat echte inhoud mist die zich bevindt in wat ooit een extreme hoek was voordat de transformatie deze ergens anders heen verplaatste. De lus uit het vorige voorbeeld uitbreiden over dezelfde States-array leest die velden rechtstreeks
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);
Delen twee Forms met dezelfde resourcenaam één lettertype?
Nee. Een resourcenaam zoals /F1 betekent alleen iets ten opzichte van de resourcedictionary die actief is op het punt waar het wordt gebruikt, en twee verschillende Form-XObjects mogen twee compleet verschillende lettertypen definiëren onder diezelfde identieke naam. PDFlibPas lost dit op door naast elke resourcenaam ook een resourcescope bij te houden: wanneer een Form zijn eigen /Resources-dictionary draagt, wordt die dictionary de complete resourcescope voor alles erbinnen, zonder per-sleutel-terugval naar de pagina- of aanroeperdictionary voor wat de eigen dictionary van de Form ook toevallig weglaat. Alleen een Form zonder enige /Resources-sleutel, een patroon dat sommige oudere PDF-generators nog steeds produceren, erft de aanroepende dictionary in zijn geheel, en dat is een doelbewuste compatibiliteitsuitzondering, geen algemene regel om op te leunen in nieuwe output. Lettertype-identiteit in een TPDFlibContentGraphicsState-momentopname is daarom het paar FontResource en FontResourceScope, niet de naam alleen, met FontObjectNumber beschikbaar om te bevestigen naar precies welk indirect object een gegeven /F1 in die specifieke scope oploste
Dezelfde scoping is van toepassing op elke andere benoemde resource die een Form kan dragen, ExtGState-items en geneste XObject-items inbegrepen, aangezien het onderliggende oplossingsmechanisme lettertypen niet als speciaal geval behandelt — het lettertypegeval doet er gewoon toevallig het meest toe, omdat een niet-overeenkomende lettertype-identiteit stilzwijgend de verkeerde glyphs produceert in plaats van een duidelijke fout. Extractiecode die tekstruns groepeert op basis van alleen de lettertypenaam, zonder ook te groeperen op resourcescope, zal twee visueel verschillende lettertypen samenvoegen die toevallig een naam delen, en de fout komt niet aan het licht totdat iemand cijfers uit het verkeerde lettertype opmerkt binnen wat verondersteld werd als één consistent lettertype te lezen
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 lezen in uw eigen pipeline
FormTraversalStatus verandert elke Do-momentopname op zichzelf al in een klein diagnostisch rapport, en een pipeline die het negeert, gooit precies de informatie weg die een onvolledige extractie zou verklaren. ftsNotApplicable betekent dat de instructie om te beginnen nooit een opgeloste Form-aanroep was; ftsNotRequested betekent dat recursie voor deze aanroep was uitgeschakeld; ftsEnumerated betekent dat de Form succesvol is geparst en doorlopen; ftsDepthLimit en ftsCycle markeren de twee manieren waarop een afdaling doelbewust wordt afgekapt; en ftsMalformed dekt al het andere dat de doorloop stopte — een onoplosbare stream-verwijzing, een /Matrix of /BBox die niet kon worden geparst, of een uitzondering opgeworpen tijdens het uitvoeren van de eigen inhoud van de Form. Dat laatste geval doet er operationeel toe, omdat een mislukte geneste doorloop terugdraait wat voor die tak ook al gedeeltelijk was geproduceerd, dus een aanroeper hoeft nooit te raden of een Form werkelijk leeg was of gewoon twee instructies in zijn inhoudsstroom ontplofte
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;
Grenzen, kosten, en waar dit past
De inhoudsstroom van een Form wordt precies één keer per opsommingsaanroep gedecodeerd en geparst, ongeacht hoe vaak de Form wordt aangeroepen, omdat PDFlibPas de geparste instructielijst cachet tegen het onderliggende stream-object in plaats van deze bij elke gelijkwaardige aanroep opnieuw te parsen — het driehoekige stempel uit het openingsvoorbeeld wordt één keer gedecodeerd en drie keer doorlopen, niet drie keer gedecodeerd. Wat wél bij elke afzonderlijke aanroep wordt herbouwd, is alles wat legitiem verschilt tussen de ene aanroepplek en de volgende: de onderliggende tracker, de samengestelde CTM, de doorsneden clip, en de resourcescope. Die CTM- en clipboekhouding per aanroep is dezelfde machinerie achter de CTM- en clipping-toestandtracker van PDFlibPas's inhoudsstroom, het waard om samen met dit artikel te lezen voor elke inhoudsstroomdoorloop die verder gaat dan Form-recursie zelf
Twee limieten zijn het waard om verwachtingen over te stellen voordat deze API in een grotere pipeline terechtkomt. Het dieteplafond van 64 niveaus is geen afstelknop voor legitiem diepe documenten, aangezien echte facturen, afschriften, en rapportsjablonen Forms in de praktijk vrijwel nooit meer dan drie of vier niveaus diep nesten — een document dat daadwerkelijk ftsDepthLimit raakt, is veel waarschijnlijker misvormd of vijandig dan ongewoon uitgebreid, en is het waard om te loggen als een datakwaliteitssignaal in plaats van stilzwijgend opnieuw te proberen met een groter getal. EnumPageContentStatesEx is ook een leeskant-analyse-API: het rapporteert wat een inhoudsstroom doet, niet of een Form überhaupt zichtbaar zou moeten zijn, wat een aparte vraag is die wordt beantwoord door de zichtbaarheidstoestand van Optional Content Groups wanneer een stempel- of watermerk-Form achter een laag zit die een viewer mogelijk heeft uitgeschakeld. Cyclusdetectie op aanroepketenniveau, isolatie per aanroep, en resourcescoping vormen samen één hoek van het inhoudsstroom-inspectieoppervlak in de PDFlibPas-component voor Delphi en C++Builder