PDFlibPas, det native VCL-PDF-komponentbibliotek til Delphi og C++Builder, genafspiller en sides content-stream gennem sin TPDFContentStateTracker-klasse uden overhovedet at røre et gengivelses-canvas. At fodre trackeren én parset operator ad gangen holder en kørende grafiktilstands-registrering — aktuel transformationsmatrix, tekst-matrix, klip-grænser, og q/Q-gem-stakken — tilgængelig til snapshot før eller efter hver operator udføres
Spørg hvor et tekstforløb rent faktisk lander på den udskrevne side, og content-strømmens rå tal alene vil vildlede dig hver gang. TPDFContentProgram.GetTextRuns rapporterer allerede hver tekst-visnings-instruktions ankerpunkt gennem OriginX- og OriginY-felterne på TPDFTextRun, og felt-kommentarerne er eksplicitte om, at det punkt sidder i tekst-rum, allerede foldet gennem Tm, Td, TD og T*. Hvad der stadig mangler, og hvad de kommentarer siger en kalder skal levere, er den CTM der er aktiv ved netop den instruktion — produktet af hver cm sammenkædet indtil videre, indlejret inde i hvor mange q/Q-par der end tilfældigvis er åbne på det punkt i strømmen
Hvorfor genafspille en content-stream i stedet for at gengive den?
PDFlibPas holder to separate begreber om grafiktilstand til to separate opgaver, og opdelingen er bevidst. Rendererens interne tilstands-registrering bærer et levende device-canvas-handle, et klip-region-handle, og skrifttype-rasteriserings-caches — rigtige ressourcer bundet til hvilken som helst overflade der aktuelt males på, og betydningsløse, når den overflade forsvinder. TPDFContentGraphicsState bærer intet af det: det er en almindelig record begrænset til de værdier, ISO 32000-1 §8.4 definerer som nåbare fra content-stream-operatorer alene — CTM'en, linjestil, farve, teksttilstand, og de afledte klip- og sti-grænser. Fordi recorden ikke holder nogen canvas-reference og intet åbent filhandle, kan en kalder parse en content-stream, gennemgå den med TPDFContentStateTracker, og blive ved med at bruge de resulterende snapshots længe efter, at hvad end der producerede bytene, er forsvundet
Hvordan TPDFContentStateTracker bygger CTM'en
TPDFContentStateTracker.Apply sammenkæder en cm-operators seks operander ind i trackerens CTM ved brug af den samme præ-multiplikation, PDF selv angiver: den nye matrix M2 kombinerer med den aktuelle CTM som M2 × CTM, i rækkevektor-konventionen hvor et punkt transformeres som P′ = P × M (ISO 32000-1 §8.4). Den del der er let at få forkert sidder i translations-termen, ikke den lineære del: M2's egen translation skal passere gennem den aktuelle CTM's rotations-og-skala-komponent, før den aktuelle CTM's translation adderes oveni. Spring det trin over og hardkod i stedet en naiv komponent-for-komponent-kombination, og den første isolerede cm man tester, vil se korrekt ud, mens hver koordinat nedstrøms af en anden eller tredje indlejret cm i stilhed driver, hvilket er præcis den slags bug, der overlever kodegennemgang, fordi enhedstesten der ville fange det, har brug for mindst to kædede transformationer for at fejle
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 besvarer smertepunktet fra åbningen: TPDFContentProgram.GetTextRuns giver OriginX og OriginY tilbage allerede foldet gennem Tm, Td, TD og T*, og TraceGraphicsStates(nil, False) leverer den ene resterende brik, den før-instruktion-CTM ved netop det indeks, hvert forløb blev fanget ved, i ét lineært gennemløb over hele programmet. At sende nil lader metoden eje en privat tracker til kaldet og frigive den internt, hvilket er det rigtige valg til en enkeltstående skanning; at sende en eksisterende TPDFContentStateTracker-instans i stedet er, hvad der holder tilstand kontinuerlig på tværs af en side samlet fra mere end én content-stream, da ISO 32000-1 behandler en sides /Contents-array som én logisk strøm, og q/Q-stakken skal stemme overens
Tekst-matricen overlever Q; grafiktilstanden gør ikke
ISO 32000-1 §9.4.2 definerer Td, TD, Tm og T* som operatorerne, der bygger tekst-matricen og tekstlinje-matricen inde i en BT/ET-blok, og PDFlibPas holder det skel skarpt: Td og TD sammenkæder en ren translation oven på tekstlinje-matricen, T* gør det samme ved brug af den negative af den aktuelle leading, og kun Tm erstatter begge matricer direkte med de seks tal, den gives. BT nulstiller begge matricer til identiteten, præcis én gang, ved starten af tekst-objektet — men q og Q rører dem slet ikke. TPDFContentStateTracker.Apply special-caser coRestoreState af netop den grund: før den popper den gemte tilstand af stakken, indfanger den den aktuelle tekst-matrix, tekstlinje-matrix og BT/ET-flag, og genanvender dem over, hvad end den poppede tilstand tilfældigvis holdt, fordi et q/Q-par pakket omkring et tekstforløb ikke skal flytte tekstpositionen tilbage
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;
Kør den sekvens, og CTM'en rapporteret ved det andet Tj er tilbage til den identitets-skala, den havde før q — 2 0 0 2 0 0 cm inde i gem/gendan-parret er væk, som q/Q kræver. TextMatrix.DX ved den samme instruktion er dog stadig 100: Td'en der satte den, kørte før q'en, så det er ikke grafiktilstand, Q'en nogensinde var berettiget til at røre, og et værktøj der antog andet, ville rapportere det andet glyf-forløb som startende fra den forkerte vandrette position på siden
Hvad sker der, når en klipsti-operator kører?
En W- eller W*-operator krymper ikke klippet med det samme; den registrerer kun, hvilken fyldregel der skal bruges, og den faktiske sammenskæring venter på hvilken som helst sti-maler-operator, der følger den, inklusive no-op-maleren n, PDF-forfattere rutinemæssigt bruger netop til at klippe uden at tegne noget. TPDFContentStateTracker spejler den to-trins-timing præcist: coClip og coClipEvenOdd sætter kun et ventende klip-regel-flag, og EndCurrentPath — kaldt af hver sti-maler-operator — er, hvad der rent faktisk skærer den ventende stis grænser ind i ClipMinX, ClipMinY, ClipMaxX og ClipMaxY. At få den iscenesættelse rigtig betyder noget for selve før/efter-snapshot-kontrakten: et før-snapshot taget præcis ved W-instruktionen skal stadig vise det gamle, bredere klip, fordi klippet ikke er trådt i kraft endnu på det punkt i strømmen, og at kollapse de to trin til ét ville i stilhed ødelægge hver kalder, der er afhængig af, at før-tilstand betyder, hvad den siger
ClipBoundsExact fortæller en kalder, hvilken af to situationer den kigger på, og den er kun nogensinde True for et enkelt akse-justeret rektangel bygget af re på en ellers tom sti — den ene form PDFlibPas kan repræsentere præcis som fire tal. Alt andet — et roteret rektangel, et kurvet omrids, en sammensat sti med flere under-stier, eller et klip bygget fra en tekst-gengivelses-tilstand — producerer stadig ClipMinX til ClipMaxY, men med ClipBoundsExact ryddet til False, et ærligt signal om, at de fire tal er en sikker ydre grænse og ikke den sande klip-form; kaldere der kun har brug for den grænse, såsom at isolere en rektangulær underregion før GDI-halvtone-nedkonverteringen beskrevet i gengivelse af PDF-sider til 1-bit monokrom, kan læse den direkte i stedet for at genudlede den fra sidens geometri
Bézier-kurver: en præcis grænse eller en sikker en
Den billigste måde at afgrænse et kubisk Bézier-segment på er at tage det konvekse hylster af dets fire kontrolpunkter, og det er altid sikkert, fordi kurven aldrig forlader det — men en lav, bred kurve kan rapportere en afgrænsningsboks langt større, end kurven rent faktisk optager, hvilket svækker klip-baseret filtrering netop, når det betyder mest, på store dekorative stier. PDFlibPas løser det stramere problem i stedet: for hver akse løser den den kubiske kurves afledte for rødder inden for det åbne interval (0, 1) og evaluerer kurven ved de rødder, den finder, sammen med begge endepunkter, hvilket er den standard lukket-form-måde at få en kurves sande akse-justerede udstrækning på frem for et overestimat. Per-kurve-præcision bæres dog ikke over til selve klippet: så snart et kurvet omrids bliver en klipsti, falder ClipBoundsExact stadig til False for det, fordi en afgrænsningsboks, uanset hvor stram, stadig ikke er den samme form som kurven, den afgrænser, og tilstands-trackeren vil hellere sige det, end lade en kalder antage et rektangel, hvor en kurve rent faktisk er
At læse tilstand før og efter hver operator
Om en kalder vil have før- eller efter-tilstanden, afhænger helt af, hvad operatoren gør: et tegne- eller hit-test-spørgsmål om en sti eller et tekstforløb vil have tilstanden, som den stod i det øjeblik før den operator kørte, da det er, hvad der rent faktisk afgjorde, hvordan operatoren malede, mens et diagnostisk spørgsmål om en tilstands-sættende operator som gs sædvanligvis vil se, hvad den lige ændrede. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) eksponerer præcis det valg som en enkelt boolean, og beregner én TPDFContentGraphicsState pr. instruktion i ét lineært gennemløb over hele programmet uanset, hvilket øjeblik der anmodes om. GetGraphicsState(InstructionIndex, AfterInstruction, State) tilbyder det samme før/efter-valg for en enkelt instruktion frem for hele programmet, men den genafspiller fra instruktion nul ved hvert kald for at nå dertil, så at skanne mange indekser ved at kalde den i en løkke koster O(n²) mod et enkelt O(n)-kald til TraceGraphicsStates over det samme program
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;
At leve med misdannede content-streams
To slags misdannet input er almindelige nok i rigtige PDF-producenter til, at TPDFContentStateTracker skal tolerere dem frem for at fejle på dem. Den første er en sti, der spænder over en q/Q-grænse: den aktuelle sti, aktuelle punkt, og under-sti-optælling er ikke grafiktilstands-parametre — ISO 32000-1 §8.4 dækker, hvad q og Q gemmer og gendanner, og den aktuelle sti, der er ved at blive bygget, er ikke blandt dem — så TPDFContentStateTracker sporer den data helt uden for den gemte tilstand, og en under-sti begyndt før et q er stadig der, umalet, umiddelbart efter det matchende Q. Den anden er et bart Q uden noget matchende q noget sted før det i strømmen, ikke sjældent i output fra generatorer, der samler content-stream-fragmenter ved sammenkædning og får regnskabet forkert. TPDFContentStateTracker.RestoreUnderflowCount tæller hver af de hændelser i stedet for at kaste en undtagelse eller ødelægge tilstand: et umatchet Q efterlader bare den aktuelle grafiktilstand nøjagtig som den var, som om den instruktion havde været en no-op, så resten af strømmen bliver ved med at genafspille på en fornuftig tilstand, og en kalder kan stadig beslutte bagefter, ud fra optællingen, om inputtet er værd at flage tilbage til hvem end der producerede det
CTM-sammensætning, tekst-matricens uafhængighed af q/Q, og den iscenesatte realisering af en klipsti afhænger ikke af, hvordan eller om content-strømmen nogensinde bliver malet, hvilket er præcis pointen: det samme TPDFContentStateTracker-snapshot er korrekt, uanset om siden aldrig gengives overhovedet, eller er ved at blive overdraget til hvilken som helst backend, PDFlibPas vælger til den fil, inklusive den runtime-motor-skiften dækket i guiden til multi-motor-PDF-gengivelse i PDFlibPas. Indholdsanalyse, koordinat-mapning og redaktions-værktøjer kan alle køre helt på trackerens output, længe før eller helt uden nogensinde at bede en renderer om at involvere sig
Content-stream-genafspilning gennem TPDFContentStateTracker er en del af den strukturerede indholds-redigerings-ramme indbygget i PDFlibPas, det native VCL-PDF-komponentbibliotek til Delphi og C++Builder