Articol tehnic

Starea fluxului de conținut PDFlibPas: urmărirea CTM și decupare

PDFlibPas, biblioteca componentei PDF VCL nativă pentru Delphi și C++Builder, redă fluxul de conținut al unei pagini prin clasa sa TPDFContentStateTracker fără a atinge deloc un canvas de randare. Alimentarea urmăritorului cu câte un operator analizat odată păstrează o înregistrare de stare grafică în curs de desfășurare — matricea de transformare curentă, matricea de text, limitele de decupare, și stiva de salvare q/Q — disponibilă pentru instantaneu înainte sau după ce fiecare operator se execută

Întrebați unde ajunge de fapt o secvență de text pe pagina tipărită, iar numerele brute ale fluxului de conținut singure vă vor induce în eroare de fiecare dată. TPDFContentProgram.GetTextRuns deja raportează punctul de ancorare al fiecărei instrucțiuni de afișare a textului prin câmpurile OriginX și OriginY de pe TPDFTextRun, iar comentariile de câmp sunt explicite că acest punct stă în spațiul textului, deja pliat prin Tm, Td, TD și T*. Ce lipsește încă, și ceea ce acele comentarii spun că un apelant trebuie să furnizeze, este CTM-ul activ la acea instrucțiune exactă — produsul fiecărui cm concatenat până acum, imbricat în interiorul oricâte perechi q/Q se întâmplă să fie deschise în acel punct al fluxului

De ce redați un flux de conținut în loc să îl randați?

PDFlibPas păstrează două noțiuni separate de stare grafică pentru două sarcini separate, iar separarea este deliberată. Înregistrarea de stare internă a renderer-ului poartă un handle de canvas de dispozitiv viu, un handle de regiune de decupare, și cache-uri de rasterizare de font — resurse reale legate de orice suprafață este pictată în prezent, și fără sens odată ce acea suprafață dispare. TPDFContentGraphicsState nu poartă nimic din toate acestea: este o simplă înregistrare limitată la valorile pe care ISO 32000-1 §8.4 le definește ca accesibile doar din operatorii de flux de conținut — CTM-ul, stilul de linie, culoarea, starea de text, și limitele de decupare și traseu derivate. Pentru că înregistrarea nu poartă nicio referință de canvas și niciun handle de fișier deschis, un apelant poate analiza un flux de conținut, îl poate parcurge cu TPDFContentStateTracker, și poate continua să folosească instantaneele rezultate mult după ce orice a produs octeții a dispărut

Cum construiește TPDFContentStateTracker CTM-ul

TPDFContentStateTracker.Apply concatenează cei șase operanzi ai unui operator cm în CTM-ul urmăritorului folosind aceeași pre-multiplicare pe care o specifică PDF-ul însuși: noua matrice M2 se combină cu CTM-ul curent ca M2 × CTM, în convenția vector-rând unde un punct se transformă ca P′ = P × M (ISO 32000-1 §8.4). Partea care este ușor de greșit stă în termenul de translație, nu în partea liniară: propria translație a lui M2 trebuie să treacă prin componenta de rotație-și-scalare a CTM-ului curent înainte ca translația CTM-ului curent să fie adăugată deasupra. Săriți peste acel pas și codificați hard în schimb o combinare naivă componentă-cu-componentă, iar primul cm izolat pe care îl testați va arăta corect, în timp ce fiecare coordonată din aval de un al doilea sau al treilea cm imbricat deviază silențios, ceea ce este exact genul de bug care supraviețuiește revizuirii codului pentru că testul unitar care l-ar prinde are nevoie de cel puțin două transformări înlănțuite pentru a eșua

var
  Prog: TPDFContentProgram;
  Runs: TPDFTextRunArray;
  States: TPDFContentGraphicsStateArray;
  DeviceX, DeviceY: Double;
  I: Integer;
begin
  Prog := TPDFContentProgram.Create;
  try
    Prog.Parse(ContentBytes);
    Runs := Prog.GetTextRuns;
    // One before-instruction snapshot per operator, computed in a single pass
    States := Prog.TraceGraphicsStates(nil, False);
    for I := 0 to High(Runs) do
    begin
      // OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
      // at this instruction is still missing (ISO 32000-1 8.4)
      with States[Runs[I].InstructionIndex].CTM do
      begin
        DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
        DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
      end;
      LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
    end;
  finally
    Prog.Free;
  end;
end;

Bucla de mai sus răspunde la punctul dureros de la deschidere: TPDFContentProgram.GetTextRuns returnează OriginX și OriginY deja pliate prin Tm, Td, TD și T*, iar TraceGraphicsStates(nil, False) furnizează piesa rămasă, CTM-ul dinainte-de-instrucțiune la indexul exact la care a fost capturată fiecare secvență, într-o singură trecere liniară peste întregul program. Transmiterea nil permite metodei să dețină un urmăritor privat pentru apel și să îl elibereze intern, ceea ce este alegerea corectă pentru o scanare unică; transmiterea unei instanțe existente TPDFContentStateTracker în schimb este ceea ce menține starea continuă pe o pagină asamblată din mai mult de un flux de conținut, întrucât ISO 32000-1 tratează tabloul /Contents al unei pagini ca un singur flux logic, iar stiva q/Q trebuie să fie de acord

Matricea de text supraviețuiește lui Q; starea grafică nu

ISO 32000-1 §9.4.2 definește Td, TD, Tm și T* ca operatorii care construiesc matricea de text și matricea de linie de text în interiorul unui bloc BT/ET, iar PDFlibPas păstrează acea distincție ascuțită: Td și TD concatenează o translație pură pe matricea de linie de text, T* face la fel folosind negativul avansului curent (leading), iar doar Tm înlocuiește ambele matrice direct cu cele șase numere care i se dau. BT resetează ambele matrice la identitate, exact o dată, la începutul obiectului de text — dar q și Q nu le ating deloc. TPDFContentStateTracker.Apply tratează coRestoreState ca un caz special exact din acest motiv: înainte de a extrage starea salvată de pe stivă, capturează matricea de text curentă, matricea de linie de text, și steagul BT/ET, și le reaplică peste orice a conținut starea extrasă, pentru că o pereche q/Q care înfășoară o secvență de text nu ar trebui să mute poziția textului înapoi

var
  Tracker: TPDFContentStateTracker;
  Prog: TPDFContentProgram;
  I: Integer;
begin
  Prog := TPDFContentProgram.Create;
  Tracker := TPDFContentStateTracker.Create;
  try
    Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
    for I := 0 to Prog.Count - 1 do
    begin
      Tracker.Apply(Prog[I]);
      if Prog[I].Op in [coShowText, coRestoreState] then
        LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
    end;
  finally
    Tracker.Free;
    Prog.Free;
  end;
end;

Rulați acea secvență, iar CTM-ul raportat la al doilea Tj este înapoi la scara de identitate pe care o avea înainte de q — 2 0 0 2 0 0 cm din interiorul perechii de salvare/restaurare a dispărut, așa cum cere q/Q. TextMatrix.DX la aceeași instrucțiune, totuși, este încă 100: Td-ul care l-a setat a rulat înainte de q, așa că nu este stare grafică pe care Q ar fi avut vreodată dreptul să o atingă, iar o unealtă care ar fi presupus altfel ar raporta a doua secvență de glife pornind de la poziția orizontală greșită pe pagină

Ce se întâmplă când rulează un operator de traseu de decupare?

Un operator W sau W* nu micșorează imediat decuparea; doar înregistrează ce regulă de umplere să folosească, iar intersecția efectivă așteaptă orice operator de pictare-traseu îl urmează, incluzând pictorul no-op n pe care autorii PDF îl folosesc de obicei exact pentru a decupa fără a desena nimic. TPDFContentStateTracker oglindește acea sincronizare în doi pași cu precizie: coClip și coClipEvenOdd doar setează un steag de regulă-de-decupare în așteptare, iar EndCurrentPath — invocat de fiecare operator de pictare-traseu — este cel care efectiv intersectează limitele traseului în așteptare în ClipMinX, ClipMinY, ClipMaxX și ClipMaxY. A obține corectă această eșalonare contează pentru chiar contractul instantaneu înainte/după: un instantaneu-înainte luat exact la instrucțiunea W tot trebuie să arate decuparea veche, mai largă, pentru că decuparea încă nu a intrat în vigoare în acel punct al fluxului, iar prăbușirea celor doi pași într-unul singur ar rupe silențios fiecare apelant care se bazează pe faptul că starea-înainte înseamnă ce spune

ClipBoundsExact spune unui apelant la care din două situații se uită, și este adevărat doar pentru un singur dreptunghi aliniat la axe construit de re pe un traseu altfel gol — singura formă pe care PDFlibPas o poate reprezenta exact ca patru numere. Orice altceva — un dreptunghi rotit, un contur curbat, un traseu compus cu mai multe subtrasee, sau o decupare construită dintr-un mod de randare a textului — tot produce ClipMinX prin ClipMaxY, dar cu ClipBoundsExact șters la False, un semnal onest că cele patru numere sunt o limită exterioară sigură, nu forma reală de decupare; apelanții care au nevoie doar de acea limită, precum izolarea unei subregiuni dreptunghiulare înainte de conversia descendentă de halftone GDI descrisă în randarea paginilor PDF la monocrom 1-bit, o pot citi direct în loc să o rederive din geometria paginii

Curbe Bézier: o limită exactă sau una sigură

Cel mai ieftin mod de a delimita un segment Bézier cubic este să luați anvelopa convexă a celor patru puncte de control ale sale, iar asta este întotdeauna sigur pentru că curba nu o părăsește niciodată — dar o curbă superficială, largă, poate raporta o casetă de delimitare mult mai mare decât ocupă efectiv curba, ceea ce slăbește filtrarea bazată pe decupare exact acolo unde contează cel mai mult, pe trasee decorative mari. PDFlibPas rezolvă în schimb problema mai strânsă: pentru fiecare axă, rezolvă derivata curbei cubice pentru rădăcini în interiorul intervalului deschis (0, 1) și evaluează curba la orice rădăcini pe care le găsește, împreună cu ambele puncte finale, ceea ce este modul standard de formă închisă de a obține adevărata întindere aliniată-la-axe a unei curbe, în loc de o supraestimare. Precizia per-curbă nu se transferă totuși la decuparea însăși: odată ce un contur curbat devine un traseu de decupare, ClipBoundsExact tot cade la False pentru el, pentru că o casetă de delimitare, oricât de strânsă, tot nu este aceeași formă ca și curba pe care o delimitează, iar urmăritorul de stare preferă să spună asta, decât să lase un apelant să presupună un dreptunghi acolo unde de fapt este o curbă

Citirea stării înainte și după fiecare operator

Dacă un apelant dorește starea-înainte sau starea-după depinde în întregime de ce face operatorul: o întrebare de desenare sau hit-testing despre un traseu sau o secvență de text vrea starea așa cum stătea în clipa dinaintea rulării acelui operator, întrucât aceasta este ceea ce a determinat efectiv cum a pictat operatorul, în timp ce o întrebare de diagnostic despre un operator de setare de stare precum gs de obicei vrea să vadă ce tocmai a schimbat. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) expune exact acea alegere ca un singur Boolean, calculând un TPDFContentGraphicsState per instrucțiune într-o singură trecere liniară peste întregul program, indiferent de ce clipă este cerută. GetGraphicsState(InstructionIndex, AfterInstruction, State) oferă aceeași alegere înainte/după pentru o singură instrucțiune în loc de întregul program, dar redă de la instrucțiunea zero la fiecare apel pentru a ajunge acolo, așa că scanarea multor indexuri apelând-o într-o buclă costă O(n²) față de un singur apel O(n) la TraceGraphicsStates peste același program

var
  Before, After: TPDFContentGraphicsState;
begin
  // Same instruction index, two different instants: before vs. after it runs
  Prog.GetGraphicsState(CmIndex, False, Before);
  Prog.GetGraphicsState(CmIndex, True, After);
  // Before.CTM reflects every earlier cm; After.CTM already folds in
  // this instruction's own concatenation as well
end;

Convețuirea cu fluxuri de conținut malformate

Două tipuri de intrare malformată sunt suficient de comune la producătorii PDF reali încât TPDFContentStateTracker trebuie să le tolereze, nu să eșueze pe ele. Prima este un traseu care se întinde peste o graniță q/Q: traseul curent, punctul curent, și numărul de subtrasee nu sunt parametri de stare grafică — ISO 32000-1 §8.4 acoperă ce salvează și restaurează q și Q, iar traseul curent în construcție nu este printre ele — așa că TPDFContentStateTracker urmărește acele date complet în afara stării salvate, iar un subtraseu început înainte de un q este încă acolo, nevopsit, imediat după Q-ul corespunzător. A doua este un Q simplu fără niciun q corespunzător nicăieri înainte de el în flux, nu rar în ieșirea de la generatoare care asamblează fragmente de flux de conținut prin concatenare și greșesc contabilitatea. TPDFContentStateTracker.RestoreUnderflowCount numără fiecare din acele evenimente, în loc să ridice o excepție sau să corupă starea: un Q nepotrivit doar lasă starea grafică curentă exact așa cum era, ca și cum acea instrucțiune ar fi fost un no-op, așa că restul fluxului continuă să se redea pe o stare sănătoasă, iar un apelant poate încă decide ulterior, din numărătoare, dacă intrarea merită semnalată înapoi cui a produs-o

Compunerea CTM, independența matricei de text față de q/Q, și realizarea eșalonată a unui traseu de decupare nu depind de cum sau dacă fluxul de conținut este vreodată pictat, ceea ce este exact ideea: același instantaneu TPDFContentStateTracker este corect fie că pagina nu este niciodată randată deloc, fie că urmează să fie predată oricărui back-end alege PDFlibPas pentru acel fișier, incluzând comutarea de motor de rulare acoperită în ghidul despre randarea PDF multi-motor în PDFlibPas. Analiza de conținut, maparea de coordonate, și uneltele de redactare pot rula toate în întregime pe ieșirea urmăritorului, mult înainte de sau complet fără a cere vreodată unui renderer să se implice

Redarea fluxului de conținut prin TPDFContentStateTracker face parte din cadrul de editare de conținut structurat încorporat în PDFlibPas, biblioteca componentei PDF VCL nativă pentru Delphi și C++Builder