PDFlibPas, det native VCL PDF-komponentbiblioteket for Delphi og C++Builder, spiller av en sides innholdsstrøm på nytt gjennom sin TPDFContentStateTracker-klasse uten å røre et gjengivelses-canvas i det hele tatt. Å mate sporeren én parset operator om gangen holder en løpende grafikktilstands-post — gjeldende transformasjonsmatrise, tekstmatrise, klippegrenser, og q/Q-lagringsstakken — tilgjengelig for øyeblikksbilde før eller etter hver operator kjører
Spør hvor et tekstforløp faktisk havner på den utskrevne siden, og innholdsstrømmens rå tall alene vil villede deg hver gang. TPDFContentProgram.GetTextRuns rapporterer allerede hver tekstvisnings-instruksjons ankerpunkt gjennom OriginX- og OriginY-feltene på TPDFTextRun, og felt-kommentarene er eksplisitte på at dette punktet sitter i tekstrom, allerede foldet gjennom Tm, Td, TD, og T*. Det som fortsatt mangler, og det kommentarene sier en kaller må levere, er CTM-en aktiv ved den nøyaktige instruksjonen — produktet av hver cm konkatenert så langt, nøstet inne i hvor mange q/Q-par som enn er åpne på det punktet i strømmen
Hvorfor spille av en innholdsstrøm på nytt i stedet for å gjengi den?
PDFlibPas holder to separate begreper om grafikktilstand for to separate jobber, og oppdelingen er bevisst. Fremviserens interne tilstandspost bærer et levende enhets-canvas-håndtak, et klippe-region-håndtak, og skrifttype-rasteriserings-buffere — ekte ressurser knyttet til hvilken overflate som enn for øyeblikket males, og meningsløse når den overflaten forsvinner. TPDFContentGraphicsState bærer ingenting av det: det er en ren record begrenset til verdiene ISO 32000-1 §8.4 definerer som nåbare fra innholdsstrøm-operatorer alene — CTM-en, linjestil, farge, teksttilstand, og de avledede klippe- og bane-grensene. Fordi recorden ikke holder noen canvas-referanse og intet åpent filhåndtak, kan en kaller parse en innholdsstrøm, gå gjennom den med TPDFContentStateTracker, og fortsette å bruke de resulterende øyeblikksbildene lenge etter at hva som enn produserte bytene, er borte
Hvordan TPDFContentStateTracker bygger CTM-en
TPDFContentStateTracker.Apply konkatenerer en cm-operators seks operander inn i sporerens CTM ved bruk av den samme premultiplikasjonen selve PDF spesifiserer: den nye matrisen M2 kombineres med den gjeldende CTM-en som M2 × CTM, i radvektor-konvensjonen der et punkt transformeres som P′ = P × M (ISO 32000-1 §8.4). Delen som er lett å få feil, sitter i translasjonsleddet, ikke den lineære delen: M2s egen translasjon må gå gjennom den gjeldende CTM-ens rotasjons-og-skala-komponent før den gjeldende CTM-ens translasjon legges til oppå. Hopp over det steget og hardkod en naiv komponentvis kombinasjon i stedet, og den første isolerte cm-en man tester, vil se korrekt ut mens hver koordinat nedstrøms for en andre eller tredje nøstet cm, stille driver, noe som er nøyaktig den typen bug som overlever kodegjennomgang fordi enhetstesten som ville fanget den, trenger minst to kjedede transformasjoner for å feile
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;
Løkken ovenfor svarer på smertepunktet fra innledningen: TPDFContentProgram.GetTextRuns gir tilbake OriginX og OriginY allerede foldet gjennom Tm, Td, TD, og T*, og TraceGraphicsStates(nil, False) leverer den ene gjenværende biten, før-instruksjon-CTM-en ved den nøyaktige indeksen hvert forløp ble fanget ved, i én enkelt lineær passering over hele programmet. Å sende nil lar metoden eie en privat sporer for kallet og frigjøre den internt, noe som er det riktige valget for en engangsskanning; å sende en eksisterende TPDFContentStateTracker i stedet er det som holder tilstanden kontinuerlig på tvers av en side satt sammen fra mer enn én innholdsstrøm, ettersom ISO 32000-1 behandler en sides /Contents-array som én logisk strøm og q/Q-stakken må stemme overens
Tekstmatrisen overlever Q; grafikktilstanden gjør ikke det
ISO 32000-1 §9.4.2 definerer Td, TD, Tm, og T* som operatorene som bygger tekstmatrisen og tekstlinje-matrisen inne i en BT/ET-blokk, og PDFlibPas holder det skillet skarpt: Td og TD konkatenerer en ren translasjon på tekstlinje-matrisen, T* gjør det samme ved bruk av det negative av gjeldende leading, og bare Tm erstatter begge matrisene helt med de seks tallene den får. BT tilbakestiller begge matrisene til identiteten, nøyaktig én gang, ved starten av tekstobjektet — men q og Q rører dem ikke i det hele tatt. TPDFContentStateTracker.Apply spesialbehandler coRestoreState nettopp av denne grunnen: før den popper den lagrede tilstanden av stakken, fanger den den gjeldende tekstmatrisen, tekstlinje-matrisen, og BT/ET-flagget, og gjenanvender dem over hva den poppede tilstanden enn holdt, fordi et q/Q-par pakket rundt et tekstforløp ikke skal flytte tekstposisjonen tilbake
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;
Kjør den sekvensen, og CTM-en rapportert ved den andre Tj-en er tilbake til identitetsskalaen den hadde før q — 2 0 0 2 0 0 cm inne i lagre/gjenopprett-paret er borte, slik q/Q krever. TextMatrix.DX ved den samme instruksjonen er derimot fortsatt 100: Td-en som satte den, kjørte før q-en, så det er ikke grafikktilstand Q noensinne hadde rett til å røre, og et verktøy som antok noe annet, ville rapportert det andre glyf-forløpet som startende fra feil horisontal posisjon på siden
Hva skjer når en klippebane-operator kjører?
En W- eller W*-operator krymper ikke klippet umiddelbart; den registrerer bare hvilken fyllregel som skal brukes, og den faktiske skjæringen venter på hvilken som helst bane-male-operator som følger den, inkludert no-op-maleren n PDF-forfattere rutinemessig bruker nettopp for å klippe uten å tegne noe. TPDFContentStateTracker speiler den to-trinns-timingen presist: coClip og coClipEvenOdd setter bare et ventende klippe-regel-flagg, og EndCurrentPath — påkalt av hver bane-male-operator — er det som faktisk skjærer den ventende banens grenser inn i ClipMinX, ClipMinY, ClipMaxX, og ClipMaxY. Å få denne iscenesettelsen riktig betyr noe for selve før/etter-øyeblikksbilde-kontrakten: et før-øyeblikksbilde tatt nøyaktig ved W-instruksjonen må fortsatt vise det gamle, bredere klippet, fordi klippet ennå ikke har trådt i kraft på det punktet i strømmen, og å slå de to trinnene sammen til ett ville stille brutt hver kaller som stoler på at før-tilstand betyr det den sier
ClipBoundsExact forteller en kaller hvilken av to situasjoner den ser på, og den er bare noensinne True for et enkelt akse-innrettet rektangel bygget av re på en ellers tom bane — den ene formen PDFlibPas kan representere eksakt som fire tall. Alt annet — et rotert rektangel, et kurvet omriss, en sammensatt bane med flere delbaner, eller et klipp bygget fra en tekst-gjengivelsesmodus — produserer fortsatt ClipMinX til ClipMaxY, men med ClipBoundsExact satt til False, et ærlig signal om at de fire tallene er en trygg ytre grense og ikke den sanne klippeformen; kallere som bare trenger den grensen, slik som å isolere et rektangulært delområde før GDI-halvtone-nedkonverteringen beskrevet i å gjengi PDF-sider til 1-bit monokrom, kan lese den direkte i stedet for å avlede den på nytt fra sidegeometrien
Bézier-kurver: en eksakt grense eller en trygg en
Den billigste måten å begrense et kubisk Bézier-segment på, er å ta det konvekse hylsteret av dets fire kontrollpunkter, og det er alltid trygt fordi kurven aldri forlater det — men en grunn, bred kurve kan rapportere en avgrensningsboks langt større enn kurven faktisk opptar, noe som svekker klipp-basert filtrering nøyaktig når det betyr mest, på store dekorative baner. PDFlibPas løser det strammere problemet i stedet: for hver akse løser den den kubiske kurvens deriverte for røtter inne i det åpne intervallet (0, 1) og evaluerer kurven ved eventuelle røtter den finner, sammen med begge endepunktene, noe som er den standard lukket-form-måten å få en kurves sanne akse-innrettede utstrekning på i stedet for en overvurdering. Per-kurve-presisjon overføres imidlertid ikke til selve klippet: så snart et kurvet omriss blir en klippebane, faller ClipBoundsExact fortsatt til False for det, fordi en avgrensningsboks, uansett hvor stram, fortsatt ikke er samme form som kurven den avgrenser, og tilstandssporeren vil heller si det enn å la en kaller anta et rektangel der en kurve faktisk er
Å lese tilstand før og etter hver operator
Om en kaller ønsker før- eller etter-tilstanden, avhenger fullstendig av hva operatoren gjør: et tegne- eller trefftest-spørsmål om en bane eller et tekstforløp ønsker tilstanden slik den sto øyeblikket før den operatoren kjørte, ettersom det er det som faktisk avgjorde hvordan operatoren malte, mens et diagnostisk spørsmål om en tilstandsettende operator som gs vanligvis ønsker å se hva den nettopp endret. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) eksponerer nettopp det valget som en enkelt boolsk verdi, og beregner én TPDFContentGraphicsState per instruksjon i én lineær passering over hele programmet uansett hvilket øyeblikk som forespørres. GetGraphicsState(InstructionIndex, AfterInstruction, State) tilbyr det samme før/etter-valget for én enkelt instruksjon i stedet for hele programmet, men den spiller av på nytt fra instruksjon null ved hvert kall for å komme dit, så å skanne mange indekser ved å kalle den i en løkke koster O(n²) mot ett enkelt O(n)-kall til TraceGraphicsStates over det samme programmet
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;
Å leve med feilformede innholdsstrømmer
To typer feilformet inndata er vanlige nok i ekte PDF-produsenter til at TPDFContentStateTracker må tolerere dem i stedet for å feile på dem. Den første er en bane som spenner over en q/Q-grense: gjeldende bane, gjeldende punkt, og delbane-antall er ikke grafikktilstands-parametere — ISO 32000-1 §8.4 dekker hva q og Q lagrer og gjenoppretter, og den gjeldende banen som bygges, er ikke blant dem — så TPDFContentStateTracker sporer den dataen helt utenfor den lagrede tilstanden, og en delbane begynt før en q, er fortsatt der, umalt, umiddelbart etter den matchende Q. Den andre er en bar Q uten noen matchende q noe sted før den i strømmen, ikke sjelden i utdata fra generatorer som setter sammen innholdsstrøm-fragmenter ved konkatenering og får regnskapsføringen feil. TPDFContentStateTracker.RestoreUnderflowCount teller hver eneste av de hendelsene i stedet for å kaste et unntak eller korrumpere tilstand: en umatchet Q etterlater bare den gjeldende grafikktilstanden nøyaktig slik den var, som om den instruksjonen hadde vært en no-op, slik at resten av strømmen fortsetter å spilles av på en fornuftig tilstand, og en kaller kan fortsatt bestemme i etterkant, fra tellingen, om inndataen er verdt å flagge tilbake til hvem som enn produserte den
CTM-komposisjon, tekstmatrisens uavhengighet fra q/Q, og den trinnvise realiseringen av en klippebane, avhenger ikke av hvordan eller om innholdsstrømmen noensinne males, noe som er nøyaktig poenget: det samme TPDFContentStateTracker-øyeblikksbildet er korrekt uansett om siden aldri gjengis i det hele tatt eller er i ferd med å overleveres til hvilken back-end PDFlibPas enn velger for den filen, inkludert kjøretids-motorbyttet dekket i guiden til multi-motor PDF-gjengivelse i PDFlibPas. Innholdsanalyse, koordinatkartlegging, og redigeringsverktøy kan alle kjøre fullstendig på sporerens utdata, lenge før eller fullstendig uten noensinne å be en fremviser om å bli involvert
Innholdsstrøm-avspilling gjennom TPDFContentStateTracker er en del av det strukturerte innholdsredigerings-rammeverket bygget inn i PDFlibPas, det native VCL PDF-komponentbiblioteket for Delphi og C++Builder