Tehnički članak

Stanje toka sadržaja PDFlibPas-a: CTM i praćenje klipa

PDFlibPas, izvorna VCL PDF biblioteka komponenti za Delphi i C++Builder, ponovo obrađuje tok sadržaja stranice kroz svoju klasu TPDFContentStateTracker, bez ikakvog dodirivanja platna za renderovanje. Dovođenje jednog po jednog parsiranog operatora do pratioca održava tekući zapis grafičkog stanja — trenutnu matricu transformacije, matricu teksta, granice klipa i stek čuvanja q/Q — dostupnim za snimak pre ili posle izvršavanja svakog operatora

Ako pitate gde se niz teksta zaista nalazi na odštampanoj stranici, sirovi brojevi iz toka sadržaja svaki put će vas navesti na pogrešan trag. TPDFContentProgram.GetTextRuns već prijavljuje sidrišnu tačku svake instrukcije koja prikazuje tekst kroz polja OriginX i OriginY objekta TPDFTextRun, a komentari polja izričito navode da se ta tačka nalazi u prostoru teksta, već obrađena kroz Tm, Td, TD i T*. Ono što i dalje nedostaje, a što ti komentari navode da pozivalac mora da obezbedi, jeste CTM aktivan pri toj tačnoj instrukciji — proizvod svih do tada konkateniranih cm operatora, ugnježden unutar onoliko q/Q parova koliko je u tom trenutku otvoreno u toku

Zašto ponovo obrađivati tok sadržaja umesto njegovog renderovanja

PDFlibPas održava dva odvojena poimanja grafičkog stanja za dva odvojena zadatka, i ta podela je namerna. Interni zapis stanja renderera sadrži živu ručku platna uređaja, ručku oblasti klipa i keš memorije za rasterizaciju fontova — stvarne resurse vezane za površinu na kojoj se trenutno crta, a besmislene kada ta površina nestane. TPDFContentGraphicsState ne sadrži ništa od toga: to je običan zapis ograničen na vrednosti koje ISO 32000-1 §8.4 definiše kao dostupne samo iz operatora toka sadržaja — CTM, stil linije, boju, stanje teksta i izvedene granice klipa i putanje. Pošto zapis ne sadrži referencu na platno niti otvorenu ručku datoteke, pozivalac može da parsira tok sadržaja, prođe kroz njega pomoću TPDFContentStateTracker-a i nastavi da koristi dobijene snimke dugo nakon što ono što je proizvelo bajtove nestane

Kako TPDFContentStateTracker gradi CTM

TPDFContentStateTracker.Apply konkatenira šest operanada cm operatora u CTM pratioca koristeći isto pre-množenje koje propisuje sam PDF: nova matrica M2 kombinuje se sa trenutnim CTM-om kao M2 × CTM, uz konvenciju vektora reda u kojoj se tačka transformiše kao P′ = P × M (ISO 32000-1 §8.4). Deo koji se lako pogrešno uradi nalazi se u članu translacije, a ne u linearnom delu: sopstvena translacija matrice M2 mora proći kroz komponentu rotacije i skaliranja trenutnog CTM-a pre nego što se preko toga doda translacija trenutnog CTM-a. Preskočite taj korak i umesto toga upišite naivnu kombinaciju komponentu po komponentu, pa će prvi izolovani cm koji testirate izgledati ispravno, dok će svaka koordinata posle drugog ili trećeg ugnježdenog cm operatora neprimetno odlutati, što je upravo vrsta greške koja preživi pregled koda jer test jedinice koji bi je otkrio mora da obuhvati najmanje dve ulančane transformacije da bi pao

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;

Gornja petlja odgovara na problem iz uvoda: TPDFContentProgram.GetTextRuns vraća OriginX i OriginY već obrađene kroz Tm, Td, TD i T*, a TraceGraphicsStates(nil, False) obezbeđuje jedini preostali deo, CTM pre instrukcije na tačnom indeksu na kom je svaki niz uhvaćen, u jednom linearnom prolazu kroz ceo program. Prosleđivanje vrednosti nil omogućava metodi da za taj poziv koristi privatnog pratioca i interno ga oslobodi, što je pravi izbor za jednokratno skeniranje; prosleđivanje postojećeg TPDFContentStateTracker-a je ono što održava stanje neprekidnim preko stranice sastavljene od više tokova sadržaja, pošto ISO 32000-1 tretira niz /Contents stranice kao jedan logički tok i q/Q stek mora da bude usklađen

Matrica teksta preživljava Q, a grafičko stanje ne

ISO 32000-1 §9.4.2 definiše Td, TD, Tm i T* kao operatore koji grade matricu teksta i matricu reda teksta unutar bloka BT/ET, a PDFlibPas tu razliku jasno održava: Td i TD konkateniraju čistu translaciju na matricu reda teksta, T* radi isto koristeći negativnu vrednost trenutnog proreda, a samo Tm u potpunosti zamenjuje obe matrice sa šest prosleđenih brojeva. BT resetuje obe matrice na identitet tačno jednom, na početku tekstualnog objekta — ali ih q i Q uopšte ne dodiruju. TPDFContentStateTracker.Apply posebno obrađuje coRestoreState upravo iz tog razloga: pre nego što skine sačuvano stanje sa steka, čuva trenutnu matricu teksta, matricu reda teksta i oznaku BT/ET, pa ih ponovo primenjuje preko vrednosti koje je izvađeno stanje možda sadržalo, jer par q/Q obavijen oko tekstualnog niza ne treba da vrati poziciju teksta unazad

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;

Pokrenite taj niz i CTM prijavljen pri drugom Tj vratiće se na identitetnu skalu koju je imao pre q — 2 0 0 2 0 0 cm unutar para čuvanja i vraćanja je nestao, kako q/Q nalaže. TextMatrix.DX pri istoj instrukciji ipak ostaje 100: Td koji ga je postavio izvršen je pre q, pa nije deo grafičkog stanja koje je Q ikada smelo da menja, a alat koji bi pretpostavio suprotno prijavio bi da drugi niz glifova počinje sa pogrešne horizontalne pozicije na stranici

Šta se dešava kada se izvrši operator putanje klipa

Operator W ili W* ne sužava klip odmah; on samo beleži koje pravilo popunjavanja treba koristiti, a stvarno presek čeka operator za iscrtavanje putanje koji sledi, uključujući no-op iscrtavač n koji autori PDF-a redovno koriste upravo za klipovanje bez ikakvog iscrtavanja. TPDFContentStateTracker precizno oponaša to dvostepeno vreme: coClip i coClipEvenOdd samo postavljaju zastavicu pravila klipa na čekanju, dok EndCurrentPath — koji poziva svaki operator za iscrtavanje putanje — zaista preseca granice putanje na čekanju sa ClipMinX, ClipMinY, ClipMaxX i ClipMaxY. Ispravno postavljanje ove faze važno je i za sam ugovor snimka pre i posle: snimak pre instrukcije uzet tačno pri W operatoru i dalje mora prikazivati stari, širi klip, jer klip u toj tački toka još nije stupio na snagu, a spajanje ta dva koraka u jedan neprimetno bi pokvarilo svakog pozivaoca koji se oslanja na to da stanje pre instrukcije znači upravo ono što kaže

ClipBoundsExact pozivaocu govori sa kojom od dve situacije ima posla i ikada je True samo za jedan pravougaonik poravnat sa osama, izgrađen operatorom re na inače praznoj putanji — jedini oblik koji PDFlibPas može tačno da predstavi kao četiri broja. Sve ostalo — rotirani pravougaonik, zakrivljeni obris, složena putanja sa više podputanja ili klip izgrađen režimom iscrtavanja teksta — i dalje proizvodi vrednosti od ClipMinX do ClipMaxY, ali sa vrednošću ClipBoundsExact postavljenom na False, što je pošten signal da su ta četiri broja bezbedna spoljašnja granica, a ne pravi oblik klipa; pozivaoci kojima je potrebna samo ta granica, na primer za izdvajanje pravougaonog podregiona pre GDI poluton-konverzije opisane u renderovanju PDF stranica u 1-bitnu monohromatsku sliku, mogu je pročitati direktno umesto da je ponovo izvode iz geometrije stranice

Bezierove krive: tačna ili bezbedna granica

Najjeftiniji način za ograničavanje kubnog segmenta Bezierove krive jeste uzimanje konveksnog omotača njene četiri kontrolne tačke, i to je uvek bezbedno jer kriva nikada ne izlazi iz njega — ali plitka, široka kriva može prijaviti okvir mnogo veći od prostora koji zaista zauzima, čime se slabi filtriranje zasnovano na klipu upravo kada je najvažnije, na velikim ukrasnim putanjama. PDFlibPas umesto toga rešava precizniji problem: za svaku osu rešava izvod kubne krive po korenima unutar otvorenog intervala (0, 1) i izračunava krivu u svim pronađenim korenima, zajedno sa obe krajnje tačke, što je standardni zatvoreni oblik za dobijanje stvarnog osno poravnatog obima krive umesto preterane procene. Preciznost po krivi ipak se ne prenosi na sam klip: kada zakrivljeni obris postane putanja klipa, ClipBoundsExact za njega i dalje prelazi na False, jer okvir, ma koliko tesan bio, ipak nije isti oblik kao kriva koju ograničava, a pratilac stanja radije to jasno kaže nego da pozivaocu dopusti da pretpostavi pravougaonik tamo gde se zapravo nalazi kriva

Čitanje stanja pre i posle svakog operatora

Da li pozivaocu treba stanje pre ili posle u potpunosti zavisi od onoga što operator radi: pitanje o crtanju ili proveri pogotka za putanju ili niz teksta zahteva stanje kakvo je bilo u trenutku neposredno pre izvršavanja operatora, jer je ono stvarno odredilo kako je operator iscrtan, dok dijagnostičko pitanje o operatoru koji postavlja stanje, kao što je gs, obično želi da vidi šta je upravo promenjeno. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) izlaže upravo taj izbor kao jednu logičku vrednost, izračunavajući jedan TPDFContentGraphicsState po instrukciji u jednom linearnom prolazu kroz ceo program, bez obzira na to koji trenutak se zahteva. GetGraphicsState(InstructionIndex, AfterInstruction, State) nudi isti izbor pre ili posle za jednu instrukciju umesto za ceo program, ali do nje dolazi ponovnom obradom od instrukcije nula pri svakom pozivu, pa skeniranje mnogih indeksa pozivanjem u petlji košta O(n²), nasuprot jednom pozivu O(n) metode TraceGraphicsStates nad istim programom

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;

Rad sa neispravnim tokovima sadržaja

Dve vrste neispravnog ulaza dovoljno su česte kod stvarnih proizvođača PDF-a da TPDFContentStateTracker mora da ih podnese umesto da zbog njih otkaže. Prva je putanja koja prelazi granicu q/Q: trenutna putanja, trenutna tačka i broj podputanja nisu parametri grafičkog stanja — ISO 32000-1 §8.4 obuhvata ono što q i Q čuvaju i vraćaju, a putanja koja se trenutno gradi nije među tim vrednostima — pa TPDFContentStateTracker te podatke u potpunosti prati izvan sačuvanog stanja, i podputanja započeta pre q i dalje postoji, neiscrtana, odmah nakon odgovarajućeg Q. Druga je samostalni Q bez odgovarajućeg q bilo gde ranije u toku, što nije retko u izlazu generatora koji spajaju delove tokova sadržaja konkatenacijom i pogrešno vode evidenciju. TPDFContentStateTracker.RestoreUnderflowCount broji svaki takav događaj umesto da podigne izuzetak ili pokvari stanje: neusaglašeni Q samo ostavlja trenutno grafičko stanje tačno onakvim kakvo je bilo, kao da je ta instrukcija bila no-op, pa se ostatak toka nastavlja obrađivati nad zdravim stanjem, a pozivalac naknadno i dalje može da odluči, na osnovu brojača, da li ulaz treba prijaviti onome ko ga je proizveo

Sastavljanje CTM-a, nezavisnost matrice teksta od q/Q i fazna realizacija putanje klipa ne zavise od toga kako se tok sadržaja iscrtava, niti da li se uopšte iscrtava, što je upravo poenta: isti snimak TPDFContentStateTracker-a ispravan je bilo da se stranica nikada ne renderuje ili da će uskoro biti prosleđena pozadini koju PDFlibPas odabere za tu datoteku, uključujući prebacivanje izvršnog mehanizma obrađeno u vodiču za renderovanje PDF-a pomoću više mehanizama u PDFlibPas-u. Analiza sadržaja, mapiranje koordinata i alati za redakciju mogu u potpunosti da rade na izlazu pratioca, mnogo pre nego što se renderer pozove ili potpuno bez njegovog uključivanja

Ponovna obrada toka sadržaja pomoću TPDFContentStateTracker-a deo je okvira za strukturisano uređivanje sadržaja ugrađenog u PDFlibPas, izvornu VCL PDF biblioteku komponenti za Delphi i C++Builder