Techninis straipsnis

PDF Library for Delphi turinio srauto būsena: CTM ir iškarpos sekimas

PDF Library for Delphi – 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

PDF Library for Delphi diagrama apie turinio srauto pakartojimo procesų grandinę: išanalizuoti PDF operatoriai maitina TPDFContentStateTracker po vieną operatorių, kuris palaiko CTM, teksto matricą, iškarpos ribas ir q/Q kaupiklį kaip momentinės kopijos įrašą
Kiekvienas išanalizuotas operatorius atnaujina seklio grafikos būsenos įrašą, o po vieną momentinę kopiją kiekvienai instrukcijai gaunama iš vieno einamojo praėjimo. Įrašas lieka tinkamas naudoti be jokios atvaizdavimo drobės

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

PDF Library for Delphi 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;
    // Po vieną būsenos kopiją prieš instrukciją kiekvienam operatoriui, apskaičiuotą vienu perėjimu
    States := Prog.TraceGraphicsStates(nil, False);
    for I := 0 to High(Runs) do
    begin
      // OriginX/OriginY jau apima Tm/Td/TD/T*; trūksta tik aktyvaus CTM
      // šioje instrukcijoje (ISO 32000-1 8.4) vis dar trūksta
      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 PDF Library for Delphi šį 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

PDF Library for Delphi pavyzdinio srauto BT, Td, q, cm, Tj, Q, Tj, ET laiko juosta su būsenų lentele, rodančia, kad Q atšaukia CTM mastelį, o teksto matricos DX lygus 100 išgyvena abu glifų paleidimus
cm komanda q/Q poros viduje padvigubina CTM tik pirmajam glifų etapui, o Q tą mastelį vėl pašalina. Td nustatyta teksto matrica išlieka, nes būsenos atkūrimas ją pritaiko iš naujo, o nepasitikima išimtuoju grafikos įrašu
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ą PDF Library for Delphi 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

PDF Library for Delphi: dviejų žingsnių iškarpos laiko juosta, kur W įrašo tik iškarpos taisyklę, sekantis kelio piešimo operatorius susikerta su laukiančiomis ribomis, o ClipBoundsExact lieka teisingas tik vienam re stačiakampiui
Iškirpimas įsigalioja, kai paleidžiamas kitas kelio piešimo operatorius, todėl momentinė kopija, padaryta prieš W, vis dar pateikia senąsias, platesnes ribas. Tik ašims lygiagreti re komanda pateikia ClipBoundsExact true, o visa kita sąžiningai degraduoja iki saugios išorinės ribos

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. PDF Library for Delphi 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
  // Tas pats instrukcijos indeksas, du skirtingi momentai: prieš ir po vykdymo
  Prog.GetGraphicsState(CmIndex, False, Before);
  Prog.GetGraphicsState(CmIndex, True, After);
  // Before.CTM atspindi kiekvieną ankstesnį cm; After.CTM jau apima
  // taip pat ir šios instrukcijos sujungimą
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 PDF Library for Delphi parinktam vidiniam varikliui, įskaitant vykdymo variklio perjungimą, aprašytą PDF Library for Delphi 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 į PDF Library for Delphi, dalis; tai savoji VCL PDF komponentų biblioteka, skirta Delphi ir C++Builder