Tehnični članak

Stanje vsebinskega toka PDFlibPas: sledenje CTM in izrezu

PDFlibPas, izvorna knjižnica komponent VCL PDF za Delphi in C++Builder, ponovi vsebinski tok strani prek razreda TPDFContentStateTracker, ne da bi se sploh dotaknila izrisnega platna. Če sledilniku podajate po en razčlenjeni operator, ta sproti vzdržuje zapis grafičnega stanja — trenutno transformacijsko matriko, besedilno matriko, meje izreza in sklad shranjevanja q/Q — ki je na voljo za posnetek pred ali po izvedbi vsakega operatorja

Če vprašate, kam se niz besedila dejansko postavi na natisnjeni strani, vas bodo surove številke vsebinskega toka vsakič zavedle. TPDFContentProgram.GetTextRuns že poroča sidrno točko vsakega ukaza za izpis besedila prek polj OriginX in OriginY v TPDFTextRun, komentarji polj pa izrecno povedo, da je ta točka v besedilnem prostoru in že vključuje Tm, Td, TD ter T*. Še vedno manjka CTM, aktivna pri tem natančnem ukazu — zmnožek vseh do tedaj združenih operatorjev cm, ugnezden znotraj poljubnega števila parov q/Q, odprtih na tem mestu toka

Zakaj ponoviti vsebinski tok namesto izrisa?

PDFlibPas vzdržuje dva ločena pojma grafičnega stanja za dve različni nalogi, razdelitev pa je namerna. Notranji zapis stanja izrisovalnika vsebuje živ ročaj naprave-platna, ročaj območja izreza in predpomnilnike rasterizacije pisav — dejanske vire, vezane na trenutno izrisano površino, ki postanejo brez pomena, ko ta površina izgine. TPDFContentGraphicsState ne vsebuje ničesar od tega: je navaden zapis, omejen na vrednosti, ki jih ISO 32000-1 §8.4 določa kot dosegljive samo z operatorji vsebinskega toka — CTM, slog črte, barvo, stanje besedila ter izpeljane meje izreza in poti. Ker zapis nima sklica na platno ali odprtega ročaja datoteke, lahko klicatelj razčleni vsebinski tok, ga prehodi s TPDFContentStateTracker in nastale posnetke uporablja še dolgo po tem, ko vir bajtov ni več na voljo

Kako TPDFContentStateTracker zgradi CTM

TPDFContentStateTracker.Apply združi šest operandov operatorja cm v CTM sledilnika z istim predmnoženjem, kot ga določa PDF: nova matrika M2 se s trenutno CTM združi kot M2 × CTM, v konvenciji vrstičnih vektorjev, kjer se točka transformira kot P′ = P × M (ISO 32000-1 §8.4). Napaka se najlažje skriva v translacijskem členu, ne v linearnem delu: lastna translacija M2 mora najprej skozi rotacijsko-lestvično komponento trenutne CTM, šele nato se prišteje translacija trenutne CTM. Če ta korak izpustite in namesto tega ročno uporabite naivno združevanje po komponentah, bo prvi izolirani cm videti pravilen, medtem ko bodo vse koordinate za drugim ali tretjim ugnezdenim cm tiho odplavale, kar je natanko vrsta napake, ki preživi pregled kode, ker jo lahko razkrije šele preizkus z vsaj dvema zaporednima transformacijama

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;

Zgornja zanka rešuje težavo iz uvoda: TPDFContentProgram.GetTextRuns vrne OriginX in OriginY, ki že vključujeta Tm, Td, TD in T*, TraceGraphicsStates(nil, False) pa v enem linearnem prehodu čez ves program zagotovi še manjkajoči del, CTM pred ukazom na natančnem indeksu, kjer je bil zajet posamezni niz. Če podate nil, metoda za klic ustvari zasebni sledilnik in ga interno sprosti, kar je prava izbira za enkratno pregledovanje; če namesto tega podate obstoječi TPDFContentStateTracker, ohranite neprekinjeno stanje čez stran, sestavljeno iz več vsebinskih tokov, saj ISO 32000-1 polje /Contents strani obravnava kot en logični tok, sklad q/Q pa se mora ujemati

Besedilna matrika preživi Q, grafično stanje pa ne

ISO 32000-1 §9.4.2 določa Td, TD, Tm in T* kot operatorje, ki znotraj bloka BT/ET gradijo besedilno matriko in matriko besedilne vrstice, PDFlibPas pa to razliko dosledno ohranja: Td in TD na matriko besedilne vrstice združita čisto translacijo, T* stori enako z negativno vrednostjo trenutnega vodila, samo Tm pa obe matriki v celoti zamenja s šestimi podanimi števili. BT obe matriki na začetku besedilnega objekta natanko enkrat ponastavi na identiteto — q in Q se ju sploh ne dotakneta. TPDFContentStateTracker.Apply zato posebej obravnava coRestoreState: preden odstrani shranjeno stanje s sklada, zajame trenutno besedilno matriko, matriko besedilne vrstice in zastavico BT/ET ter jih znova uporabi čez vrednosti, ki jih je vsebovalo odstranjeno stanje, ker par q/Q okoli besedila ne sme premakniti položaja besedila nazaj

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;

Če izvedete to zaporedje, se CTM pri drugem Tj vrne na identično lestvico, ki jo je imela pred q — 2 0 0 2 0 0 cm znotraj para shranjevanja in obnovitve izgine, kot zahteva q/Q. TextMatrix.DX je pri tem istem ukazu še vedno 100: Td, ki ga je nastavil, se je izvedel pred q, zato ne gre za grafično stanje, ki bi se ga Q smel dotakniti, orodje, ki bi predpostavilo nasprotno, pa bi poročalo, da se drugi niz glifov začne na napačnem vodoravnem položaju strani

Kaj se zgodi ob izvedbi operatorja poti izreza

Operator W ali W* izreza ne zmanjša takoj; samo zabeleži pravilo polnjenja, dejansko presečišče pa počaka na naslednji operator za izris poti, vključno z operatorjem brez izrisa n, ki ga avtorji PDF pogosto uporabijo prav za izrez brez risanja. TPDFContentStateTracker natančno posnema ta dvostopenjski časovni potek: coClip in coClipEvenOdd nastavita le čakajočo zastavico pravila izreza, EndCurrentPath — ki ga prikliče vsak operator za izris poti — pa meje čakajoče poti dejansko preseka z vrednostmi ClipMinX, ClipMinY, ClipMaxX in ClipMaxY. Pravilno zaporedje je pomembno tudi za pogodbo posnetkov pred in po ukazu: posnetek pred ukazom W mora še vedno pokazati star, širši izrez, ker takrat v toku še ni začel veljati, združitev obeh korakov pa bi tiho pokvarila vsakega klicatelja, ki se zanaša na pomen stanja pred ukazom

ClipBoundsExact klicatelju pove, katero od dveh situacij opazuje, True pa je samo pri eni osno poravnani pravokotnici, zgrajeni z re na sicer prazni poti — pri edini obliki, ki jo PDFlibPas lahko natančno predstavi s štirimi števili. Vse drugo — zasukan pravokotnik, ukrivljen obris, sestavljena pot z več podpotmi ali izrez, zgrajen iz načina izrisa besedila — še vedno ustvari ClipMinX do ClipMaxY, vendar je ClipBoundsExact nastavljena na False, kar pošteno pove, da so štiri številke varna zunanja meja in ne prava oblika izreza; klicatelji, ki potrebujejo samo to mejo, na primer pri osamitvi pravokotnega podobmočja pred pretvorbo GDI v poltonski izris, opisano v izrisu strani PDF v 1-bitni enobarvni obliki, jo lahko preberejo neposredno, namesto da bi jo znova izpeljevali iz geometrije strani

Bézierjeve krivulje: natančna ali varna meja

Najcenejši način omejevanja kubičnega odseka Bézierjeve krivulje je konveksna ovojnica njegovih štirih kontrolnih točk, kar je vedno varno, ker je krivulja nikoli ne zapusti — vendar lahko plitva in široka krivulja vrne omejevalni okvir, precej večji od dejansko zasedenega prostora, kar oslabi filtriranje na podlagi izreza prav takrat, ko je najpomembnejše, pri velikih okrasnih poteh. PDFlibPas namesto tega rešuje natančnejšo nalogo: za vsako os poišče ničle odvoda kubične krivulje znotraj odprtega intervala (0, 1) in krivuljo ovrednoti v najdenih ničlah ter na obeh krajiščih, kar je standardni zaprti način za pravi osno poravnani obseg namesto precenjene meje. Natančnost posamezne krivulje pa se ne prenese na sam izrez: ko ukrivljeni obris postane pot izreza, ClipBoundsExact zanj še vedno preide na False, saj omejevalni okvir, naj bo še tako tesen, ni enak obliki krivulje, ki jo omejuje, sledilnik stanja pa to raje jasno pove, kot da klicatelju dovoli domnevati pravokotnik tam, kjer je dejansko krivulja

Branje stanja pred in po vsakem operatorju

Ali klicatelj potrebuje stanje pred ali po ukazu, je povsem odvisno od njegovega učinka: vprašanje izrisa ali preverjanja zadetka za pot oziroma niz besedila potrebuje stanje tik pred izvedbo operatorja, ker je prav to določilo njegov izris, diagnostično vprašanje o operatorju za nastavitev stanja, kot je gs, pa navadno želi videti, kaj je ta pravkar spremenil. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) to izbiro izpostavi kot eno samo logično vrednost in v enem linearnem prehodu čez ves program izračuna en TPDFContentGraphicsState na ukaz, ne glede na zahtevani trenutek. GetGraphicsState(InstructionIndex, AfterInstruction, State) ponudi enako izbiro pred ali po ukazu za posamezni ukaz, vendar se pri vsakem klicu znova predvaja od ukaza nič, zato pregled številnih indeksov v zanki stane O(n²), namesto enega klica O(n) za 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;

Delo z neustrezno oblikovanimi vsebinskimi tokovi

V resničnih izdelovalcih PDF sta dovolj pogosti dve vrsti neustreznih vhodov, da ju mora TPDFContentStateTracker prenesti, namesto da bi ob njiju odpovedal. Prva je pot, ki prečka mejo q/Q: trenutna pot, trenutna točka in število podpot niso parametri grafičnega stanja — ISO 32000-1 §8.4 opisuje, kaj q in Q shranita ter obnovita, trenutna pot v gradnji pa ni med tem — zato TPDFContentStateTracker te podatke v celoti vodi zunaj shranjenega stanja in podpot, začeta pred q, ostane takoj po ujemajočem se Q neizrisana prisotna. Druga je samostojen Q brez ujemajočega q pred njim v toku, kar pri izhodu generatorjev, ki fragmente vsebinskega toka sestavljajo z združevanjem in se zmotijo pri knjigovodstvu, ni redko. TPDFContentStateTracker.RestoreUnderflowCount prešteje vsak tak dogodek, namesto da bi vrgel izjemo ali poškodoval stanje: neusklajeni Q pusti trenutno grafično stanje natančno takšno, kot je bilo, kot da bi bil ukaz brez učinka, zato se preostanek toka še naprej predvaja v veljavnem stanju, klicatelj pa lahko pozneje iz števila presodi, ali mora vhod prijaviti njegovemu izdelovalcu

Sestavljanje CTM, neodvisnost besedilne matrike od q/Q in postopna uveljavitev poti izreza niso odvisni od tega, ali se vsebinski tok sploh kdaj izriše, kar je bistvo pristopa: isti posnetek TPDFContentStateTracker je pravilen, če stran nikoli ni izrisana ali če bo tik pred predajo zaledju, ki ga PDFlibPas izbere za to datoteko, vključno s preklapljanjem izvajalnega pogona, opisanem v vodniku za izris PDF z več pogoni v PDFlibPas. Analiza vsebine, preslikava koordinat in orodja za redakcijo lahko v celoti delujejo na izhodu sledilnika, dolgo pred vključitvijo izrisovalnika ali povsem brez nje

Predvajanje vsebinskega toka prek TPDFContentStateTracker je del ogrodja za strukturirano urejanje vsebine, vgrajenega v PDFlibPas, izvorno knjižnico komponent VCL PDF za Delphi in C++Builder