Techninis straipsnis

PDFlibPas turinio srauto būsena: CTM ir iškarpos sekimas

PDFlibPas – savoji VCL PDF komponentų biblioteka, skirta Delphi ir C++Builder, atkuria puslapio turinio srautą naudodama klasę TPDFContentStateTracker ir visiškai neliečia atvaizdavimo drobės. Pateikus sekikliui po vieną išanalizuotą operatorių, nuolat atnaujinamas grafinės būsenos įrašas – dabartinė transformacijos matrica, teksto matrica, iškarpos ribos ir q/Q išsaugojimo dėklas – todėl jį galima užfiksuoti prieš vykdant kiekvieną operatorių arba po jo

Jei norite sužinoti, kur tiksliai išspausdintame puslapyje atsiduria teksto eilutė, vien tik neapdoroti turinio srauto skaičiai kaskart jus suklaidins. TPDFContentProgram.GetTextRuns jau pateikia kiekvienos tekstą rodančios instrukcijos atramos tašką per TPDFTextRun laukus OriginX ir OriginY, o laukų komentarai aiškiai nurodo, kad šis taškas yra teksto erdvėje ir jau apskaičiuotas pagal Tm, Td, TD bei T*. Vis dar trūksta, ir tai turi pateikti iškvietėjas, tiksliai tai instrukcijai aktyvios CTM – visų iki tol sujungtų cm sandauga, esanti tiek atvirų q/Q porų sluoksnių, kiek tuo metu yra sraute

Kodėl turinio srautą reikia atkurti, o ne atvaizduoti

PDFlibPas naudoja dvi atskiras grafinės būsenos sampratas dviem skirtingoms užduotims, ir toks atskyrimas yra sąmoningas. Vidiniame atvaizduoklio būsenos įraše saugoma aktyvi įrenginio drobės nuoroda, iškarpos srities nuoroda ir šriftų rastravimo podėliai – tikri ištekliai, susieti su tuo paviršiumi, į kurį tuo metu piešiama, ir prarandantys prasmę paviršiui išnykus. TPDFContentGraphicsState to neturi: tai paprastas įrašas, apribotas reikšmėmis, kurias ISO 32000-1 §8.4 apibrėžia kaip pasiekiamas vien iš turinio srauto operatorių – CTM, linijos stilius, spalva, teksto būsena ir išvestos iškarpos bei kelio ribos. Kadangi įraše nėra drobės nuorodos ar atidaryto failo deskriptoriaus, iškvietėjas gali išanalizuoti turinio srautą, pereiti jį su TPDFContentStateTracker ir naudoti gautus momentinius vaizdus dar ilgai po to, kai baitus sukūręs šaltinis išnyksta

Kaip TPDFContentStateTracker sukuria CTM

TPDFContentStateTracker.Apply sujungia šešis cm operatoriaus operandus su sekiklio CTM naudodama tą pačią PDF nurodytą išankstinio dauginimo taisyklę: nauja matrica M2 su dabartine CTM jungiama kaip M2 × CTM, taikant eilučių vektorių susitarimą, kuriame taškas transformuojamas taip: P′ = P × M (ISO 32000-1 §8.4). Lengva suklysti ne tiesine dalimi, o transliacijos dėmeniu: pačios M2 transliacijos vektorius turi būti transformuotas per dabartinės CTM pasukimo ir mastelio komponentą, o tik tada prie jo pridedama dabartinės CTM transliacija. Praleidus šį veiksmą ir vietoje jo įrašius naivų komponentinį derinį, pirmas atskirai išbandytas cm atrodys teisingas, tačiau kiekviena koordinatė po antrojo ar trečiojo įdėto cm tyliai nukryps, ir tai yra būtent toks riktas, kuris išgyvena kodo peržiūrą, nes jį aptinkantis vieneto testas turi patikrinti bent dvi susietas transformacijas

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;

Aukščiau pateikta kilpa išsprendžia pradžioje aprašytą problemą: TPDFContentProgram.GetTextRuns grąžina OriginX ir OriginY, jau apskaičiuotus pagal Tm, Td, TD bei T*, o TraceGraphicsStates(nil, False) pateikia vienintelę trūkstamą dalį – prieš instrukciją esančią CTM tiksliai ties tuo indeksu, kuriame buvo užfiksuotas kiekvienas paleidimas, ir tai atlieka vienu tiesiniu praėjimu per visą programą. Perdavus nil, metodas šiam iškvietimui susikuria privatų sekiklį ir pats jį atlaisvina, todėl tai tinkamas pasirinkimas vienkartiniam nuskaitymui; perdavus esamą TPDFContentStateTracker, būsena išlieka tęstinė puslapyje, sudarytame iš daugiau nei vieno turinio srauto, nes ISO 32000-1 puslapio /Contents masyvą traktuoja kaip vieną loginį srautą, o q/Q dėklas turi sutapti

Teksto matrica išlieka per Q, o grafinė būsena – ne

ISO 32000-1 §9.4.2 apibrėžia Td, TD, Tm ir T* kaip operatorius, kurie BT/ET bloke sudaro teksto matricą ir teksto eilutės matricą, o PDFlibPas šį skirtumą griežtai išlaiko: Td ir TD prie teksto eilutės matricos prijungia grynąją transliaciją, T* atlieka tą patį naudodamas neigiamą dabartinį eilučių intervalą, o tik Tm visiškai pakeičia abi matricas šešiais jam pateiktais skaičiais. BT teksto objekto pradžioje tiksliai vieną kartą atstato abi matricas į tapatybės matricą, tačiau q ir Q jų visiškai neliečia. TPDFContentStateTracker.Apply specialiai apdoroja coRestoreState būtent dėl šios priežasties: prieš nuimdamas išsaugotą būseną nuo dėklo jis išsaugo dabartinę teksto matricą, teksto eilutės matricą ir BT/ET požymį, tada pritaiko juos virš to, kas buvo iššoktoje būsenoje, nes q/Q pora, apgaubianti teksto paleidimą, neturi grąžinti teksto padėties

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;

Paleidus šią seką, antrojo Tj metu pranešama CTM vėl turi tapatybės mastelį, buvusį prieš q – 2 0 0 2 0 0 cm išsaugojimo ir atkūrimo poroje išnyko, kaip reikalauja q/Q. Tačiau TextMatrix.DX tos pačios instrukcijos metu vis dar yra 100: jį nustačiusi Td buvo įvykdyta prieš q, todėl tai nėra grafinė būsena, kurią Q būtų galėjusi pakeisti, ir priešingai mananti priemonė praneštų, kad antrasis glifų paleidimas puslapyje prasideda neteisingoje horizontalioje padėtyje

Kas nutinka vykdant iškarpos kelio operatorių

W arba W* operatorius iškarpos iš karto nesumažina; jis tik įrašo, kurią užpildo taisyklę naudoti, o tikroji sankirta laukiama iki po jo einančio kelio piešimo operatoriaus, įskaitant neveikiantį piešiklį n, kurį PDF autoriai įprastai naudoja būtent iškirpti nieko nepiešiant. TPDFContentStateTracker tiksliai atkartoja šį dviejų veiksmų laiką: coClip ir coClipEvenOdd tik nustato laukiančios iškarpos taisyklės požymį, o EndCurrentPath, iškviečiamas kiekvieno kelio piešimo operatoriaus, iš tikrųjų įtraukia laukiančio kelio ribas į ClipMinX, ClipMinY, ClipMaxX ir ClipMaxY sankirtą. Teisingas šis etapų išdėstymas svarbus pačiai būsenos prieš ir po fiksavimo sutarčiai: prieš būsenos momentinis vaizdas, paimtas tiksliai ties W instrukcija, vis dar turi rodyti seną, platesnę iškarpą, nes tuo metu sraute iškarpa dar neįsigaliojo, o sujungus abu veiksmus į vieną būtų tyliai sugadinti visi iškvietėjai, kuriems prieš būsena turi reikšti būtent tai, ką sako

ClipBoundsExact nurodo iškvietėjui, su kuria iš dviejų situacijų jis susiduria, ir reikšmę True įgyja tik vienam ašims lygiagrečiam stačiakampiui, sudarytam re operatoriumi kitaip tuščiame kelyje – tai vienintelė forma, kurią PDFlibPas gali tiksliai išreikšti keturiais skaičiais. Visa kita – pasuktas stačiakampis, lenktas kontūras, sudėtinis kelias su keliais pokeliais ar iš teksto atvaizdavimo režimo sudaryta iškarpa – vis tiek sukuria reikšmes nuo ClipMinX iki ClipMaxY, tačiau ClipBoundsExact nustato į False, sąžiningai nurodydama, kad keturi skaičiai yra saugi išorinė riba, o ne tikroji iškarpos forma; iškvietėjai, kuriems reikia tik šios ribos, pavyzdžiui, prieš GDI pustonių mažinimą izoliuojantys stačiakampę sritį, aprašytą PDF puslapių atvaizdavimo 1 bito vienspalviu režimu vadove, gali ją perskaityti tiesiogiai, o ne iš naujo išvesti iš puslapio geometrijos

Bézier kreivės: tiksli ar saugi riba

Pigiausias būdas apriboti kubinį Bézier segmentą – paimti jo keturių valdymo taškų iškilųjį apvalkalą, ir tai visada saugu, nes kreivė niekada jo nepalieka, tačiau plati ir negili kreivė gali pateikti daug didesnį aprėpiantį stačiakampį, nei iš tikrųjų užima pati kreivė, todėl iškarpa paremtas filtravimas susilpnėja būtent tada, kai jo labiausiai reikia, dideliuose dekoratyviniuose keliuose. PDFlibPas vietoje to sprendžia tikslesnę problemą: kiekvienai ašiai jis randa kubinės kreivės išvestinės šaknis atvirame intervale (0, 1) ir apskaičiuoja kreivę ties kiekviena rasta šaknimi bei abiejuose galuose – tai standartinis uždarosios formos būdas gauti tikrą ašims lygiagretų kreivės dydį, o ne pervertį. Tačiau atskiros kreivės tikslumas nepersikelia į pačią iškarpą: kai lenktas kontūras tampa iškarpos keliu, ClipBoundsExact vis tiek nustatomas į False, nes net ir labai tiksli aprėpianti dėžė nėra tokios pačios formos kaip jos apribota kreivė, ir būsenos sekiklis verčiau tai aiškiai nurodo, nei leidžia iškvietėjui manyti, kad ten yra stačiakampis, kai iš tikrųjų yra kreivė

Būsenos skaitymas prieš ir po kiekvieno operatoriaus

Ar iškvietėjui reikia būsenos prieš operatorių ar po jo, visiškai priklauso nuo to, ką operatorius atlieka: piešimo ar pataikymo tikrinimo klausimui apie kelią arba teksto paleidimą reikia būsenos, buvusios akimirką prieš operatoriaus vykdymą, nes būtent ji nulėmė, kaip operatorius piešė, o diagnostiniam klausimui apie būseną nustatantį operatorių, pavyzdžiui, gs, paprastai reikia matyti, ką jis ką tik pakeitė. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) tiksliai atskleidžia šį pasirinkimą kaip vieną Boolean reikšmę ir vienu tiesiniu praėjimu per visą programą apskaičiuoja po vieną TPDFContentGraphicsState kiekvienai instrukcijai, nesvarbu, kurio momento prašoma. GetGraphicsState(InstructionIndex, AfterInstruction, State) tą patį pasirinkimą prieš ar po pateikia vienai instrukcijai, o ne visai programai, tačiau kiekvieno iškvietimo metu iki jos atkartoja srautą nuo nulinės instrukcijos, todėl daugelio indeksų nuskaitymas cikle kainuoja O(n²), palyginti su vienu O(n) TraceGraphicsStates iškvietimu tai pačiai programai

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;

Darbas su netaisyklingais turinio srautais

Du netinkamai suformuotų įvesčių tipai realiuose PDF kūrimo įrankiuose pasitaiko pakankamai dažnai, kad TPDFContentStateTracker turi juos toleruoti, o ne dėl jų sustoti. Pirmasis – kelias, kertantis q/Q ribą: dabartinis kelias, dabartinis taškas ir pokelių skaičius nėra grafinės būsenos parametrai – ISO 32000-1 §8.4 aprašo, ką q ir Q išsaugo bei atkuria, o kuriamas dabartinis kelias į tai neįeina, todėl TPDFContentStateTracker šiuos duomenis seka visiškai už išsaugotos būsenos ribų, ir prieš q pradėtas pokelis vis dar lieka neįpieštas iškart po atitinkamo Q. Antrasis – atskiras Q, kurio sraute niekur anksčiau neatitinka q; tai nėra reta generatorių, kurie sujungia turinio srauto fragmentus konkatenacija ir neteisingai tvarko apskaitą, išvestyje. TPDFContentStateTracker.RestoreUnderflowCount suskaičiuoja kiekvieną tokį įvykį, užuot kėlęs išimtį ar sugadinęs būseną: neatitinkantis Q tiesiog palieka dabartinę grafinę būseną nepakeistą, tarsi ši instrukcija būtų nieko neatliekanti, todėl likusi srauto dalis atkuriama iš sveikos būsenos, o iškvietėjas vėliau pagal skaitiklį gali nuspręsti, ar verta pranešti įvesties kūrėjui apie problemą

CTM sudarymas, teksto matricos nepriklausomybė nuo q/Q ir etapinis iškarpos kelio įgyvendinimas nepriklauso nuo to, kaip ar ar apskritai turinio srautas kada nors atvaizduojamas, ir būtent tai yra esmė: tas pats TPDFContentStateTracker momentinis vaizdas teisingas tiek tada, kai puslapis niekada neatvaizduojamas, tiek tada, kai jis netrukus perduodamas bet kuriam PDFlibPas parinktam vidiniam varikliui, įskaitant vykdymo variklio perjungimą, aprašytą PDFlibPas kelių variklių PDF atvaizdavimo vadove. Turinio analizė, koordinačių atvaizdavimas ir redagavimo įrankiai gali veikti vien pagal sekiklio išvestį, gerokai anksčiau arba visiškai neprašydami atvaizduoklio įsitraukti

Turinio srauto atkūrimas per TPDFContentStateTracker yra struktūrinio turinio redagavimo sistemos, įdiegtos į PDFlibPas, dalis; tai savoji VCL PDF komponentų biblioteka, skirta Delphi ir C++Builder