Articol tehnic

Recursivitate Form XObject: detecția ciclurilor PDFlibPas în Delphi

PDFlibPas rezolvă apelurile recursive Form XObject în fluxurile de conținut PDF Delphi urmărind lanțul de apeluri activ, nu un set global de vizitate, astfel încât TPDFlib.EnumPageContentStatesEx poate parcurge același Form invocat de mai multe ori pe o pagină fără a confunda refolosirea legitimă cu un ciclu. Un Form XObject de ștampilă într-un șablon de factură este cazul tipic: același obiect este apelat din antet, subsol și un strat de filigran pe o pagină, iar doar un lanț de apeluri care se întoarce asupra lui însuși este un ciclu autentic

ISO 32000-1 §8.10 definește un Form XObject ca un flux de conținut de sine stătător pe care o pagină, sau un alt Form, îl invocă cu operatorul Do, complet cu propriul sistem de coordonate în /Matrix, o graniță de decupare în acel sistem de coordonate în /BBox, și opțional propriul dicționar de resurse. Nimic din specificație nu plafonează de câte ori poate fi invocat un Form sau cât de adânc se pot invoca Form-urile unele pe altele, așa că un parser conform trebuie să accepte refolosirea legitimă și imbricarea legitimă în timp ce se apără totuși împotriva singurei aranjări pe care specificația chiar o interzice: un Form al cărui flux de conținut, direct sau tranzitiv, se invocă pe el însuși. PDFlibPas raportează acea distincție prin valorile TPDFlibContentFormTraversalStatus atașate fiecărui instantaneu Do, cel mai notabil ftsEnumerated pentru o coborâre reușită și ftsCycle pentru singurul caz care este efectiv o buclă

De ce nu declanșează refolosirea aceluiași Form XObject un fals ciclu?

O referință Form XObject repetată nu este, de una singură, dovadă a ceva greșit. ISO 32000-1 permite ca același obiect Form să fie invocat din câte locuri dorește autorul într-un flux de conținut, ceea ce este exact modul în care o ștampilă de logo, un șablon de antet, sau un subsol de număr de pagină este refolosit pe o pagină fără a duplica fluxul său de conținut de mai multe ori. Garda naivă împotriva recursiei incontrolabile este un singur set de vizitate cheiat de numărul de obiect: prima dată când un parcurgător vede obiectul Form 12, marchează 12 ca văzut și refuză să intre din nou în el oriunde altundeva în arbore. Acea abordare se rupe în momentul în care aceeași ștampilă apare în două colțuri fără legătură ale unei pagini, pentru că al doilea apel, complet legitim, sosește după ce numărul de obiect este deja marcat văzut și este respins ca și cum ar fi o buclă

PDFlibPas evită acel fals pozitiv delimitând detecția de ciclu la lanțul de apel curent, nu la întregul document. EnumPageContentStatesEx împinge fluxul Form rezolvat pe lanțul de apel activ imediat înainte de a coborî în el, apoi extrage aceeași intrare din nou în momentul în care coborârea revine, cu succes sau nu. O invocare-soră a fluxului identic începe doar după ce prima a fost deja extrasă, așa că lanțul de apel este curat de acel flux până când apelul-soră îl verifică, iar parcurgătorul îl enumeră exact așa cum ar face-o pentru orice alt Form. Un ciclu real arată diferit pe același lanț: Form A apelează Form B, B este încă deschis pe lanț când propriul său conținut apelează înapoi în A, iar A stă încă pe lanț din apelul exterior care nu a revenit încă — asta este singura formă pe care ftsCycle o raportează, un flux Form încă deschis undeva mai devreme pe lanțul de apel curent, nu doar prezent undeva altundeva pe pagină

Cât de adânc poate merge recursivitatea Form XObject înainte ca PDFlibPas să o oprească?

Detecția de ciclu și limitarea de adâncime rezolvă două probleme diferite, iar PDFlibPas le păstrează ca două rezultate TPDFlibContentFormTraversalStatus diferite exact din acest motiv. Un lanț de douăzeci de Form-uri distincte, fiecare apelând următorul și niciunul repetându-se, nu este un ciclu prin nicio definiție — verificarea lanțului activ nu găsește niciodată un flux repetat — dar douăzeci de niveluri oneste de imbricare tot sunt douăzeci de niveluri de analiză, concatenare de matrice, și rezolvare de resursă pe care un PDF malformat sau adversarial le-ar putea împinge arbitrar mai sus dacă nimic altceva nu le-ar opri. EnumPageContentStatesEx ia un parametru MaxFormDepth exact din acest motiv și limitează orice valoare este transmisă la un maxim de 64, indiferent de ce cere apelantul. O adâncime de zero este un caz special care merită cunoscut de unul singur: dezactivează complet recursivitatea Form și reproduce comportamentul plat, doar-pagină, al metodei mai vechi EnumPageContentStates, motiv pentru care fiecare instantaneu Do în acel mod raportează ftsNotRequested în loc să încerce orice

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;

Un sub-urmăritor per invocare: izolarea stării grafice

Fiecare coborâre într-un Form XObject primește propriul urmăritor de stare grafică, în loc să îl împartă cu cel care deja parcurge pagina, pentru că fluxul de conținut al unui Form este obligat să lase starea grafică exact așa cum a găsit-o, iar PDFlibPas nu poate presupune că fiecare PDF pe care îl deschide chiar respectă acea cerință. Urmăritorul copil pornește dintr-un instantaneu al oricărui CTM, stare de culoare, și parametri de text erau active la instrucțiunea Do apelantă, apoi resetează propria sa stivă de salvare-și-restaurare și urmărire a traseului curent la gol înainte de a executa o singură instrucțiune a Form-ului. Un q nebalansat fără niciun Q corespunzător în interiorul unui Form neglijent sau deteriorat, nu un lucru rar de găsit în PDF-uri produse de instrumente mai vechi, rămâne conținut în interiorul urmăritorului acelei unice invocări și nu se scurge niciodată în urmăritorul de pagină sau într-o invocare-soră a aceleiași ștampile așezată o linie mai jos în fluxul de conținut

/Matrix-ul Form-ului se compune cu CTM-ul în vigoare la Do la fel cum o face un operator cm, înmulțit-la-stânga față de transformarea curentă, nu substituit ei, iar PDFlibPas refolosește deliberat acea unică cale de cod, în loc să mențină o a doua formulă, întrucât două implementări independente ale aceleiași algebre de matrice sunt exact genul de duplicare care se depărtează silențios după câteva runde de compunere de scalare, rotire, și forfecare. /BBox apoi decupează în propriul spațiu de coordonate al Form-ului după ce matricea a fost deja aplicată, iar toate cele patru colțuri ale acelei casete sunt transformate individual, nu doar colțurile opuse, întrucât un Form rotit sau forfecat altfel poate raporta o casetă de delimitare care ratează conținut real așezat în ce era anterior un colț extrem înainte ca transformarea să îl mute altundeva. Extinderea buclei din exemplul anterior peste același tablou States citește acele câmpuri direct

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);

Împart două Form-uri cu același nume de resursă un singur font?

Nu. Un nume de resursă precum /F1 înseamnă ceva doar relativ la dicționarul de resurse activ în punctul unde este folosit, iar două Form XObject-uri diferite sunt libere să definească două fonturi complet diferite sub acel nume identic. PDFlibPas rezolvă asta urmărind un domeniu de resurse alături de fiecare nume de resursă: când un Form poartă propriul dicționar /Resources, acel dicționar devine domeniul complet de resurse pentru tot ce este în interiorul lui, fără nicio rezervă per-cheie la pagină sau dicționarul apelant pentru orice se întâmplă să omită propriul dicționar al Form-ului. Doar un Form fără nicio cheie /Resources deloc, un tipar încă produs de unele generatoare PDF mai vechi, moștenește dicționarul apelant în întregime, iar asta este o excepție de compatibilitate deliberată, nu o regulă generală pe care merită să vă bazați în ieșirea nouă. Identitatea fontului într-un instantaneu TPDFlibContentGraphicsState este deci perechea de FontResource și FontResourceScope, nu numele singur, cu FontObjectNumber disponibil pentru a confirma exact la ce obiect indirect s-a rezolvat un anumit /F1 în acel domeniu particular

Aceeași delimitare se aplică fiecărei alte resurse numite pe care o poate purta un Form, intrări ExtGState și intrări XObject imbricate incluse, întrucât mecanismul de rezolvare de bază nu tratează fonturile ca un caz special — cazul fontului doar se întâmplă să conteze cel mai mult, pentru că o identitate de font nepotrivită produce silențios glifele greșite, în loc de un eșec evident. Codul de extracție care grupează secvențe de text doar după numele fontului, fără a grupa și după domeniul de resurse, va îmbina două fonturi vizual diferite care se întâmplă să împartă un nume, iar greșeala nu va apărea la suprafață până când cineva observă cifre din tipul de literă greșit așezate în interiorul a ceea ce trebuia să se citească ca un singur font consistent

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);

Citirea FormTraversalStatus în propria dvs. conductă

FormTraversalStatus transformă fiecare instantaneu Do într-un mic raport de diagnostic de unul singur, iar o conductă care îl ignoră aruncă exact informația care ar explica o extracție incompletă. ftsNotApplicable înseamnă că instrucțiunea nu a fost niciodată o invocare de Form rezolvată în primul rând; ftsNotRequested înseamnă că recursivitatea a fost oprită pentru acest apel; ftsEnumerated înseamnă că Form-ul a fost analizat și parcurs cu succes; ftsDepthLimit și ftsCycle marchează cele două moduri în care o coborâre este întreruptă intenționat; iar ftsMalformed acoperă tot restul care a oprit parcurgerea — o referință de flux nerezolvabilă, un /Matrix sau /BBox care a eșuat la analiză, sau o excepție ridicată în timp ce se executa propriul conținut al Form-ului. Acel ultim caz contează operațional, pentru că o parcurgere imbricată eșuată anulează orice ieșire parțială pe care o produsese deja pentru acea ramură, așa că un apelant nu trebuie niciodată să ghicească dacă un Form era gol cu adevărat sau pur și simplu a explodat la două instrucțiuni în propriul flux de conținut

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;

Limite, costuri, și unde se potrivește asta

Fluxul de conținut al unui Form este decodat și analizat exact o dată per apel de enumerare, indiferent de câte ori este invocat Form-ul, pentru că PDFlibPas pune în cache lista de instrucțiuni analizate față de obiectul de flux de bază, în loc să o re-analizeze la fiecare apel-soră — ștampila cu trei colțuri din exemplul de deschidere este decodată o dată și parcursă de trei ori, nu decodată de trei ori. Ce se reconstruiește chiar la fiecare invocare individuală este tot ce diferă legitim de la un loc de apel la următorul: urmăritorul copil, CTM-ul concatenat, decuparea intersectată, și domeniul de resurse. Acea contabilitate de CTM și decupare per invocare este același mecanism din spatele urmăritorului de stare CTM și decupare al fluxului de conținut al PDFlibPas, care merită citit alături de acesta pentru orice parcurgere de flux de conținut care merge dincolo de recursivitatea Form-ului însăși

Două limite merită stabilite ca așteptări înainte ca acest API să intre într-o conductă mai mare. Plafonul de adâncime de 64 de niveluri nu este un buton de reglaj pentru documente legitim adânci, întrucât facturile, extrasele, și șabloanele de raport reale practic nu imbrică niciodată Form-uri mai mult de trei sau patru niveluri adânc — un document care chiar atinge ftsDepthLimit este mult mai probabil să fie malformat sau adversarial decât neobișnuit de elaborat, și merită înregistrat ca un semnal de calitate a datelor, nu reîncercat silențios cu un număr mai mare. EnumPageContentStatesEx este de asemenea un API de analiză pe partea de citire: raportează ce face un flux de conținut, nu dacă un Form ar trebui să fie vizibil deloc, ceea ce este o întrebare separată la care se răspunde prin starea de vizibilitate a Grupurilor de Conținut Opțional atunci când o ștampilă sau un Form de filigran stă în spatele unui strat pe care un vizualizator l-ar fi putut dezactiva. Detecția de ciclu pe lanțul de apel, izolarea per invocare, și delimitarea de resurse formează împreună un colț al suprafeței de inspecție a fluxului de conținut din componenta PDFlibPas pentru Delphi și C++Builder