PDFlibPas, izvorna VCL biblioteka PDF komponenti za Delphi i C++Builder, ponovno izvodi content stream stranice kroz klasu TPDFContentStateTracker bez ikakvog dodirivanja platna za renderiranje. Predajom jednog raščlanjenog operatora trackeru zadržava se tekući zapis grafičkog stanja — trenutna transformacijska matrica, tekstualna matrica, granice isječka i stog za spremanje q/Q — dostupan za snimku prije ili nakon izvršavanja svakog operatora
Postavite li pitanje gdje se niz teksta zapravo pojavljuje na ispisanoj stranici, sirovi brojevi content streama svaki će vas put navesti na pogrešan trag. TPDFContentProgram.GetTextRuns već prijavljuje sidrišnu točku svake naredbe koja ispisuje tekst kroz polja OriginX i OriginY na TPDFTextRun, a komentari polja izričito kažu da je ta točka u tekstualnom prostoru i da je već izračunata kroz Tm, Td, TD i T*. Ono što još nedostaje i što prema tim komentarima pozivatelj mora dodati jest CTM aktivan upravo u toj naredbi — umnožak svih dosad spojenih cm operatora, ugniježđen unutar onoliko parova q/Q koliko ih je u tom trenutku otvoreno u streamu
Zašto ponovno izvoditi content stream umjesto renderiranja?
PDFlibPas održava dva odvojena poimanja grafičkog stanja za dva odvojena posla, i ta je podjela namjerna. Interni zapis stanja renderera sadržava aktivnu ručku platna uređaja, ručku područja isječka i predmemorije za rasterizaciju fontova — stvarne resurse povezane s površinom na kojoj se trenutačno crta i besmislene nakon njezina nestanka. TPDFContentGraphicsState ne sadržava ništa od toga: to je običan zapis ograničen na vrijednosti za koje ISO 32000-1 §8.4 definira da su dohvatljive samo operatorima content streama — CTM, stil linije, boja, tekstualno stanje te izvedene granice isječka i putanje. Budući da zapis ne drži referencu na platno ni otvorenu datoteku, pozivatelj može raščlaniti content stream, proći kroz njega pomoću TPDFContentStateTracker i nastaviti koristiti dobivene snimke dugo nakon nestanka onoga što je proizvelo bajtove
Kako TPDFContentStateTracker gradi CTM
TPDFContentStateTracker.Apply spaja šest operanada operatora cm u CTM trackera koristeći isto prethodno množenje koje propisuje sam PDF: nova matrica M2 kombinira se s trenutačnim CTM-om kao M2 × CTM, prema konvenciji vektora retka u kojoj se točka transformira kao P′ = P × M (ISO 32000-1 §8.4). Dio koji je lako pogrešno izračunati nalazi se u članu translacije, a ne u linearnom dijelu: vlastita translacija matrice M2 mora proći kroz komponentu rotacije i skaliranja trenutačnog CTM-a prije nego što se na vrh doda njegova postojeća translacija. Preskočite taj korak i umjesto toga unaprijed zadajte naivnu kombinaciju po komponentama, pa će prvi izolirani cm koji testirate izgledati ispravno, dok će svaka koordinata nakon drugog ili trećeg ugniježđenog cm tiho odlutati, upravo onakva pogreška koja preživi pregled koda jer test koji bi je otkrio mora sadržavati najmanje dvije ulančane transformacije
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ć izračunate kroz Tm, Td, TD i T*, a TraceGraphicsStates(nil, False) u jednom linearnom prolazu kroz cijeli program daje posljednji nedostajući dio, CTM prije naredbe na točnom indeksu na kojem je svaki niz uhvaćen. Predaja vrijednosti nil omogućuje metodi da za poziv upotrijebi privatni tracker i sama ga oslobodi, što je pravi izbor za jednokratno skeniranje; predaja postojećeg TPDFContentStateTracker umjesto toga održava stanje neprekinutim kroz stranicu sastavljenu od više content streamova, jer ISO 32000-1 tretira polje stranice /Contents kao jedan logički stream i stog q/Q mora ostati usklađen
Tekstualna matrica preživljava Q, a grafičko stanje ne
ISO 32000-1 §9.4.2 definira Td, TD, Tm i T* kao operatore koji grade tekstualnu matricu i matricu retka teksta unutar bloka BT/ET, a PDFlibPas tu razliku strogo čuva: Td i TD spajaju čistu translaciju s matricom retka teksta, T* čini isto koristeći negativnu vrijednost trenutačnog vodećeg razmaka, a samo Tm izravno zamjenjuje obje matrice sa svojih šest zadanih brojeva. BT vraća obje matrice na identitet, točno jednom na početku tekstualnog objekta, ali q i Q ih uopće ne diraju. TPDFContentStateTracker.Apply posebno obrađuje coRestoreState upravo zbog toga: prije nego što skine spremljeno stanje sa stoga, hvata trenutačnu tekstualnu matricu, matricu retka teksta i oznaku BT/ET, zatim ih ponovno primjenjuje preko onoga što je izvađeno stanje možda sadržavalo, jer par q/Q oko niza teksta ne smije vratiti položaj teksta unatrag
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;
Pokrenete li taj slijed, CTM prijavljen pri drugom Tj vraća se na identitetsko skaliranje koje je imao prije q — 2 0 0 2 0 0 cm unutar para spremanja i vraćanja nestaje, kako q/Q zahtijeva. Ipak, TextMatrix.DX u istoj naredbi i dalje iznosi 100: Td koji ga je postavio izvršio se prije q, pa to nije grafičko stanje koje je Q ikada smio dirati, a alat koji bi pretpostavio suprotno prijavio bi da drugi niz glifova počinje na pogrešnom vodoravnom položaju na stranici
Što se događa kada se izvrši operator putanje isječka?
Operator W ili W* ne sužava isječak odmah; on samo bilježi koje pravilo ispune treba upotrijebiti, a stvarno presijecanje čeka sljedeći operator za iscrtavanje putanje, uključujući operator bez učinka n koji autori PDF-a često koriste upravo za izrezivanje bez crtanja. TPDFContentStateTracker precizno oponaša to dvokoračno tempiranje: coClip i coClipEvenOdd samo postavljaju oznaku pravila isječka, a EndCurrentPath — koji poziva svaki operator za iscrtavanje putanje — stvarno presijeca granice pending putanje u ClipMinX, ClipMinY, ClipMaxX i ClipMaxY. Ispravno određivanje tog redoslijeda važno je i za ugovor snimaka prije i poslije: snimka prije naredbe uzeta točno na operatoru W još mora pokazivati stari, širi isječak jer se isječak u tom trenutku streama još nije primijenio, a spajanje ta dva koraka u jedan neprimjetno bi pokvarilo svakog pozivatelja koji očekuje da stanje prije naredbe znači upravo ono što kaže
ClipBoundsExact pozivatelju govori koju od dvije situacije gleda, a vrijednost je True samo za jedan osno poravnati pravokutnik izgrađen operatorom re na inače praznoj putanji — jedini oblik koji PDFlibPas može točno predstaviti s četiri broja. Sve ostalo — rotirani pravokutnik, zakrivljeni obris, složena putanja s više podputanja ili isječak izgrađen načinom iscrtavanja teksta — i dalje daje ClipMinX do ClipMaxY, ali s postavljenom vrijednošću ClipBoundsExact False, što iskreno signalizira da su četiri broja sigurna vanjska granica, a ne stvarni oblik isječka; pozivatelji kojima treba samo ta granica, poput izdvajanja pravokutnog podpodručja prije GDI-jeve pretvorbe polutonova opisane u renderiranju PDF stranica u 1-bitnu monokromatsku sliku, mogu je čitati izravno umjesto da je ponovno izvode iz geometrije stranice
Bézierove krivulje: točna ili sigurna granica
Najjeftiniji način omeđivanja kubične Bézierove dionice jest uzeti konveksnu ljusku njezine četiri kontrolne točke, a to je uvijek sigurno jer krivulja nikada ne izlazi iz nje — ali plitka, široka krivulja može prijaviti granični okvir mnogo veći od stvarne površine koju zauzima, čime filtriranje prema isječku slabi upravo kada je najpotrebnije, na velikim ukrasnim putanjama. PDFlibPas umjesto toga rješava uži problem: za svaku os rješava derivaciju kubične krivulje za korijene unutar otvorenog intervala (0, 1) i izračunava krivulju u pronađenim korijenima zajedno s obje krajnje točke, što je standardni zatvoreni oblik za dobivanje stvarnog osno poravnatog dosega krivulje umjesto precijenjene granice. Preciznost pojedine krivulje ipak se ne prenosi na sam isječak: kada zakrivljeni obris postane putanja isječka, ClipBoundsExact se i dalje postavlja na False jer granični okvir, koliko god tijesan bio, nije isti oblik kao krivulja koju omeđuje, a tracker stanja radije to jasno kaže nego da pozivatelju dopusti pretpostaviti pravokutnik ondje gdje je zapravo krivulja
Čitanje stanja prije i poslije svakog operatora
Želi li pozivatelj stanje prije ili poslije u potpunosti ovisi o tome što operator radi: pitanje crtanja ili testiranja pogotka o putanji ili nizu teksta traži stanje kakvo je bilo neposredno prije izvršavanja operatora jer je ono stvarno odredilo kako je operator iscrtao sadržaj, dok dijagnostičko pitanje o operatoru koji postavlja stanje, poput gs, obično želi vidjeti što je upravo promijenio. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) izlaže taj izbor kao jedan Boolean, računajući po jedan TPDFContentGraphicsState za svaku naredbu u jednom linearnom prolazu kroz cijeli program bez obzira na to koji se trenutak traži. GetGraphicsState(InstructionIndex, AfterInstruction, State) nudi isti izbor prije ili poslije za jednu naredbu umjesto za cijeli program, ali pri svakom pozivu ponovno izvodi program od naredbe nulte, pa skeniranje mnogih indeksa pozivanjem u petlji ima cijenu O(n²), nasuprot jednom pozivu TraceGraphicsStates cijene O(n) 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 s neispravnim content streamovima
Dvije vrste neispravnog ulaza dovoljno su česte u stvarnim proizvođačima PDF-a da ih TPDFContentStateTracker mora tolerirati umjesto da na njima ne uspije. Prva je putanja koja prelazi granicu q/Q: trenutačna putanja, trenutačna točka i broj podputanja nisu parametri grafičkog stanja — ISO 32000-1 §8.4 opisuje što q i Q spremaju i vraćaju, a putanja koja se trenutačno gradi nije među tim vrijednostima — pa TPDFContentStateTracker te podatke prati potpuno izvan spremljenog stanja, a podputanja započeta prije q i dalje postoji, neiscrtana, odmah nakon odgovarajućeg Q. Druga je samostalni Q bez odgovarajućeg q bilo gdje ranije u streamu, što nije rijetko u izlazu generatora koji spajaju odsječke content streama i pogriješe u evidenciji. TPDFContentStateTracker.RestoreUnderflowCount broji svaki takav događaj umjesto da podigne iznimku ili pokvari stanje: neusuglašeni Q ostavlja trenutačno grafičko stanje potpuno kakvo je bilo, kao da je ta naredba bila bez učinka, pa se ostatak streama i dalje izvodi nad ispravnim stanjem, a pozivatelj naknadno iz broja može odlučiti treba li ulaz prijaviti onome tko ga je proizveo
Sastavljanje CTM-a, neovisnost tekstualne matrice o q/Q i odgođena realizacija putanje isječka ne ovise o tome hoće li se content stream ikada iscrtati ni na koji način, što je upravo poanta: ista snimka TPDFContentStateTracker ispravna je bilo da se stranica nikada ne renderira ili da će se predati pozadini koju PDFlibPas odabere za tu datoteku, uključujući prebacivanje runtime enginea obrađeno u vodiču za renderiranje PDF-a u više enginea u PDFlibPasu. Analiza sadržaja, preslikavanje koordinata i alati za redigiranje mogu se u cijelosti osloniti na izlaz trackera mnogo prije renderiranja ili bez ikakvog pozivanja renderera
Ponavljanje content streama kroz TPDFContentStateTracker dio je okvira za strukturirano uređivanje sadržaja ugrađenog u PDFlibPas, izvornu VCL biblioteku PDF komponenti za Delphi i C++Builder