PDFlibPas, natiivi VCL-PDF-komponenttikirjasto Delphille ja C++Builderille, toistaa sivun sisältövirran TPDFContentStateTracker-luokkansa kautta koskematta renderöintikangastaan lainkaan. Yhden jäsennetyn operaattorin syöttäminen seurantalaitteeseen kerrallaan pitää juoksevan graafisen tilan tietueen — nykyisen muunnosmatriisin, tekstimatriisin, rajausrajat ja q/Q-tallennuspinon — saatavilla tilannekuvaksi ennen jokaista operaattoria tai sen jälkeen
Kysy, mihin tekstin ajo todella osuu tulostetulla sivulla, ja pelkät sisältövirran raakaluvut johtavat sinut harhaan joka kerta. TPDFContentProgram.GetTextRuns antaa jo takaisin jokaisen tekstinäyttöohjeen ankkuripisteen OriginX- ja OriginY-kenttien kautta TPDFTextRun-oliossa, ja kenttien kommentit ovat selviä siitä, että tämä piste istuu tekstitilassa, jo taitettuna Tm:n, Td:n, TD:n ja T*:n läpi. Se, mikä yhä puuttuu, ja mitä nuo kommentit sanovat kutsujan täytyvän toimittaa, on aktiivinen CTM juuri tuolla ohjeella — jokaisen tähän mennessä ketjutetun cm:n tulo, sisäkkäisenä minkä tahansa määrän q/Q-parien sisällä, jotka sattuvat olemaan auki tuossa kohdassa virtaa
Miksi toistaa sisältövirta sen renderöimisen sijaan?
PDFlibPas pitää kaksi erillistä käsitettä graafisesta tilasta kahta erillistä tehtävää varten, ja jako on tarkoituksellinen. Renderöijän sisäinen tilatietue kantaa elävän laitekangaskahvan, rajausaluekahvan ja fontin rasterointivälimuistit — todellisia resursseja, jotka on sidottu mihin tahansa pintaan, jota juuri nyt maalataan, ja merkityksettömiä heti, kun tuo pinta katoaa. TPDFContentGraphicsState ei kanna mitään tuosta: se on tavallinen tietue, rajattu arvoihin, jotka ISO 32000-1 §8.4 määrittelee tavoitettaviksi pelkästään sisältövirran operaattoreista — CTM, viivatyyli, väri, tekstitila sekä johdetut rajaus- ja polkurajat. Koska tietueella ei ole kangasviittausta eikä avointa tiedostokahvaa, kutsuja voi jäsentää sisältövirran, kävellä sen läpi TPDFContentStateTracker-luokalla, ja jatkaa syntyneiden tilannekuvien käyttöä kauan sen jälkeen, kun se, mikä tuotti tavut, on kadonnut
Miten TPDFContentStateTracker rakentaa CTM:n
TPDFContentStateTracker.Apply yhdistää cm-operaattorin kuusi operandia seurantalaitteen CTM:ään käyttäen samaa esikertolaskua, jonka itse PDF määrittää: uusi matriisi M2 yhdistyy nykyisen CTM:n kanssa muodossa M2 × CTM, rivivektorikäytännössä, jossa piste muunnetaan muodossa P′ = P × M (ISO 32000-1 §8.4). Osa, joka on helppo saada väärin, istuu siirtymätermissä, ei lineaarisessa osassa: M2:n oman siirtymän on kuljettava nykyisen CTM:n kierto-ja-skaalaus-komponentin läpi, ennen kuin nykyisen CTM:n siirtymä lisätään sen päälle. Ohita tuo vaihe ja kovakoodaa naiivi komponenttikohtainen yhdistelmä sen sijaan, ja ensimmäinen eristetty cm, jota testaat, näyttää oikealta, kun taas jokainen koordinaatti toisen tai kolmannen sisäkkäisen cm:n alavirrassa ajautuu hiljaa väärään, mikä on juuri sellainen bugi, joka selviää koodikatselmuksesta, koska yksikkötesti, joka nappaisi sen, tarvitsee vähintään kaksi ketjutettua muunnosta epäonnistuakseen
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;
Yllä oleva silmukka vastaa avauksen kipupisteeseen: TPDFContentProgram.GetTextRuns antaa takaisin OriginX- ja OriginY-arvot jo taitettuina Tm:n, Td:n, TD:n ja T*:n läpi, ja TraceGraphicsStates(nil, False) toimittaa sen yhden puuttuvan palan, ennen-ohjetta-CTM:n täsmälleen sillä indeksillä, jolla kukin ajo vangittiin, yhdessä lineaarisessa kierroksessa koko ohjelman yli. nil:n välittäminen antaa metodin omistaa yksityisen seurantalaitteen kutsulle ja vapauttaa sen sisäisesti, mikä on oikea valinta kertaluonteiselle skannaukselle; olemassa olevan TPDFContentStateTracker-instanssin välittäminen sen sijaan on se, mikä pitää tilan jatkuvana sivun yli, joka on koottu useammasta kuin yhdestä sisältövirrasta, koska ISO 32000-1 kohtelee sivun /Contents-taulukkoa yhtenä loogisena virtana, ja q/Q-pinon on oltava samaa mieltä
Tekstimatriisi selviää Q:sta; graafinen tila ei
ISO 32000-1 §9.4.2 määrittelee Td:n, TD:n, Tm:n ja T*:n operaattoreiksi, jotka rakentavat tekstimatriisin ja tekstirivimatriisin BT/ET-lohkon sisällä, ja PDFlibPas pitää tuon eron terävänä: Td ja TD yhdistävät puhtaan siirtymän tekstirivimatriisiin, T* tekee saman käyttäen nykyisen liu'un negatiivia, ja vain Tm korvaa molemmat matriisit kokonaan kuudella luvulla, jotka sille annetaan. BT nollaa molemmat matriisit identiteettiin, täsmälleen kerran, tekstiolio alussa — mutta q ja Q eivät kosketa niitä lainkaan. TPDFContentStateTracker.Apply käsittelee coRestoreState-tapauksen erikoistapauksena juuri tästä syystä: ennen kuin se poimii tallennetun tilan pois pinosta, se vangitsee nykyisen tekstimatriisin, tekstirivimatriisin ja BT/ET-lipun, ja soveltaa ne uudelleen sen päälle, mitä poimittu tila sattui kantamaan, koska q/Q-pari, joka on kääritty tekstiajon ympärille, ei ole tarkoitettu siirtämään tekstipositiota takaisin
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;
Aja tuo sekvenssi, ja toisessa Tj:ssä raportoitu CTM on takaisin siinä identiteettiskaalassa, joka sillä oli ennen q:ta — 2 0 0 2 0 0 cm tallennus/palautus-parin sisällä on poissa, kuten q/Q vaatii. TextMatrix.DX samassa ohjeessa on kuitenkin yhä 100: Td, joka asetti sen, ajettiin ennen q:ta, joten se ei ole graafista tilaa, jota Q oli koskaan oikeutettu koskettamaan, ja työkalu, joka olettaisi toisin, raportoisi toisen glyfiajon alkavan väärästä vaakasijainnista sivulla
Mitä tapahtuu, kun rajauspolkuoperaattori ajetaan?
W- tai W*-operaattori ei kutista rajausta välittömästi; se vain kirjaa, mitä täyttösääntöä käyttää, ja todellinen leikkaus odottaa mitä tahansa polkua maalaavaa operaattoria, joka seuraa sitä, mukaan lukien ei-toiminto-maalaaja n, jota PDF-tekijät käyttävät rutiininomaisesti juuri rajatakseen piirtämättä mitään. TPDFContentStateTracker peilaa tuon kaksivaiheisen ajoituksen tarkasti: coClip ja coClipEvenOdd asettavat vain odottavan rajaussääntölipun, ja EndCurrentPath — kutsuttuna jokaisesta polkua maalaavasta operaattorista — on se, mikä todella leikkaa odottavan polun rajat kenttiin ClipMinX, ClipMinY, ClipMaxX ja ClipMaxY. Tämän vaiheistuksen saaminen oikein on merkityksellistä itse ennen/jälkeen-tilannekuvasopimukselle: ennen-tilannekuva, joka on otettu täsmälleen W-ohjeen kohdalla, on silti näytettävä vanha, leveämpi rajaus, koska rajaus ei ole vielä astunut voimaan siinä kohdassa virtaa, ja näiden kahden vaiheen romahduttaminen yhdeksi rikkoisi hiljaa jokaisen kutsujan, joka luottaa siihen, että ennen-tila tarkoittaa sitä, mitä se sanoo
ClipBoundsExact kertoo kutsujalle, kumpaa kahdesta tilanteesta se katsoo, ja se on aina vain True yhdelle akselinsuuntaiselle suorakulmiolle, jonka re rakensi muuten tyhjälle polulle — ainoalle muodolle, jonka PDFlibPas voi esittää tarkasti neljänä lukuna. Kaikki muu — kierretty suorakulmio, kaareva ääriviiva, yhdistelmäpolku useilla alipoluilla, tai rajaus, joka on rakennettu tekstirenderöintitilasta — tuottaa silti ClipMinX:stä ClipMaxY:hyn, mutta ClipBoundsExact tyhjennettynä False:ksi, rehellinen signaali siitä, että nuo neljä lukua ovat turvallinen ulkoraja eivätkä todellinen rajausmuoto; kutsujat, jotka tarvitsevat vain tuon rajan, kuten suorakulmaisen osa-alueen eristämisen ennen GDI-puolisävytealennuskonversiota, joka kuvataan artikkelissa PDF-sivujen renderöinti 1-bittiseksi mustavalkoiseksi, voivat lukea sen suoraan sen sijaan, että johtaisivat sen uudelleen sivun geometriasta
Bézier-käyrät: tarkka raja tai turvallinen raja
Halvin tapa rajata kuutiollinen Bézier-segmentti on ottaa sen neljän kontrollipisteen kupera kuori, ja se on aina turvallinen, koska käyrä ei koskaan poistu siitä — mutta matala, leveä käyrä voi raportoida rajauslaatikon, joka on paljon suurempi kuin käyrä todella miehittää, mikä heikentää rajauspohjaista suodatusta juuri silloin, kun se on tärkeintä, suurilla koristeellisilla poluilla. PDFlibPas ratkaisee sen sijaan tiukemman ongelman: jokaiselle akselille se ratkaisee kuutiollisen käyrän derivaatan juuret avoimen välin (0, 1) sisällä ja arvioi käyrän missä tahansa löytämissään juurissa, yhdessä molempien päätepisteiden kanssa, mikä on vakiomuotoinen suljetun muodon tapa saada käyrän todellinen akselinsuuntainen laajuus yliarvion sijaan. Käyräkohtainen tarkkuus ei kuitenkaan siirry itse rajaukseen: heti kun kaarevasta ääriviivasta tulee rajauspolku, ClipBoundsExact yhä putoaa False:ksi sille, koska rajauslaatikko, olipa se kuinka tiukka tahansa, ei silti ole sama muoto kuin käyrä, jota se rajaa, ja tilaseurantalaite mieluummin sanoo niin kuin antaisi kutsujan olettaa suorakulmiota siellä, missä todella on käyrä
Tilan lukeminen ennen ja jälkeen jokaisen operaattorin
Se, haluaako kutsuja ennen- vai jälkeen-tilan, riippuu kokonaan siitä, mitä operaattori tekee: piirto- tai osumatestauskysymys polusta tai tekstiajosta haluaa tilan sellaisena kuin se seisoi hetkeä ennen kuin tuo operaattori ajettiin, koska se on se, mikä todella määritti, miten operaattori maalasi, kun taas diagnostinen kysymys tilaa asettavasta operaattorista kuten gs haluaa yleensä nähdä, mitä se juuri muutti. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) paljastaa juuri tuon valinnan yhtenä totuusarvona, laskien yhden TPDFContentGraphicsState-tietueen per ohje yhdessä lineaarisessa kierroksessa koko ohjelman yli riippumatta siitä, kumpi hetki pyydetään. GetGraphicsState(InstructionIndex, AfterInstruction, State) tarjoaa saman ennen/jälkeen-valinnan yhdelle ohjeelle koko ohjelman sijaan, mutta se toistaa ohjeesta nolla joka kutsulla päästäkseen sinne, joten monen indeksin skannaaminen kutsumalla sitä silmukassa maksaa O(n²) yhtä O(n)-kutsua vastaan TraceGraphicsStates-metodille saman ohjelman yli
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;
Eläminen virheellisten sisältövirtojen kanssa
Kaksi virheellisen syötteen tyyppiä ovat riittävän yleisiä todellisissa PDF-tuottajissa, että TPDFContentStateTracker joutuu sietämään niitä eikä epäonnistumaan niiden takia. Ensimmäinen on polku, joka ulottuu q/Q-rajan yli: nykyinen polku, nykyinen piste ja alipolkumäärä eivät ole graafisen tilan parametreja — ISO 32000-1 §8.4 kattaa, mitä q ja Q tallentavat ja palauttavat, eikä rakenteilla oleva nykyinen polku ole niiden joukossa — joten TPDFContentStateTracker seuraa tuota dataa kokonaan tallennetun tilan ulkopuolella, ja alipolku, joka on aloitettu ennen q:ta, on yhä siellä, maalaamattomana, välittömästi vastaavan Q:n jälkeen. Toinen on paljas Q ilman vastaavaa q:ta missään ennen sitä virrassa, ei harvinaista tulosteessa generaattoreista, jotka kokoavat sisältövirtafragmentteja ketjuttamalla ja saavat kirjanpidon väärin. TPDFContentStateTracker.RestoreUnderflowCount laskee jokaisen tällaisen tapahtuman poikkeuksen nostamisen tai tilan vioittamisen sijaan: täsmäämätön Q vain jättää nykyisen graafisen tilan täsmälleen sellaiseksi kuin se oli, ikään kuin tuo ohje olisi ollut ei-toiminto, joten virran loppuosa jatkaa toistoa järkevällä tilalla, ja kutsuja voi silti päättää jälkikäteen, laskurin perusteella, kannattaako syöte merkitä takaisin sille, joka sen tuotti
CTM-kompositio, tekstimatriisin riippumattomuus q/Q:sta ja rajauspolun vaiheittainen toteutuminen eivät riipu siitä, miten tai renderöidäänkö sisältövirta koskaan, mikä on juuri pointti: sama TPDFContentStateTracker-tilannekuva on oikea riippumatta siitä, ei sivua koskaan renderöidä lainkaan vai ollaanko se juuri antamassa mille tahansa taustajärjestelmälle, jonka PDFlibPas valitsee tuolle tiedostolle, mukaan lukien ajonaikaisen moottorin vaihto, joka käsitellään artikkelissa opas moni-moottoriseen PDF-renderöintiin PDFlibPas:ssa. Sisällön analysointi, koordinaattien kartoitus ja redaktointityökalut voivat kaikki toimia kokonaan seurantalaitteen tulosteen varassa, kauan ennen tai täysin ilman koskaan pyytämättä renderöijää mukaan
Sisältövirran toisto TPDFContentStateTracker-luokan kautta on osa rakenteellista sisällönmuokkauskehystä, joka on rakennettu PDFlibPas:aan, natiiviin VCL-PDF-komponenttikirjastoon Delphille ja C++Builderille