PDFlibPas, det nativa VCL PDF-komponentbiblioteket för Delphi och C++Builder, spelar upp en sidas innehållsström genom sin TPDFContentStateTracker-klass utan att röra en rendreringsyta alls. Att mata spåraren en tolkad operator i taget håller en löpande grafiktillståndspost — aktuell transformationsmatris, textmatris, klippgränser, och q/Q-sparstacken — tillgänglig för ögonblicksbild före eller efter varje operator exekverar
Fråga var en textkörning faktiskt hamnar på den utskrivna sidan, och innehållsströmmens råa tal ensamma kommer lura dig varje gång. TPDFContentProgram.GetTextRuns rapporterar redan varje textvisande instruktions ankarpunkt genom OriginX- och OriginY-fälten på TPDFTextRun, och fältkommentarerna är tydliga med att den punkten sitter i textutrymme, redan vikt genom Tm, Td, TD, och T*. Vad som fortfarande saknas, och vad de kommentarerna säger att en anropare måste tillhandahålla, är CTM:en aktiv vid exakt den instruktionen — produkten av varje cm sammanfogad hittills, kapslad inuti hur många q/Q-par som än råkar vara öppna vid den punkten i strömmen
Varför spela upp en innehållsström istället för att rendrera den?
PDFlibPas håller två separata begrepp om grafiktillstånd för två separata jobb, och uppdelningen är medveten. Rendrerarens interna tillståndspost bär ett levande enhetsyta-handtag, ett klippregionshandtag, och typsnittsrasteriseringscacher — riktiga resurser bundna till vilken yta som än för närvarande målas, och meningslösa när den ytan väl försvinner. TPDFContentGraphicsState bär inget av det: det är en vanlig post begränsad till värdena ISO 32000-1 §8.4 definierar som nåbara enbart från innehållsströmsoperatorer — CTM:en, linjestil, färg, texttillstånd, och de härledda klipp- och baningsgränserna. Eftersom posten inte håller någon ytareferens och inget öppet filhandtag kan en anropare tolka en innehållsström, gå igenom den med TPDFContentStateTracker, och fortsätta använda de resulterande ögonblicksbilderna långt efter att vad som än producerade byten försvunnit
Hur TPDFContentStateTracker bygger CTM:en
TPDFContentStateTracker.Apply sammanfogar en cm-operators sex operander in i spårarens CTM med samma förmultiplicering PDF självt specificerar: den nya matrisen M2 kombineras med den aktuella CTM:en som M2 × CTM, i radvektor-konventionen där en punkt transformeras som P′ = P × M (ISO 32000-1 §8.4). Delen som är lätt att få fel sitter i translationstermen, inte den linjära delen: M2:s egen translation måste passera genom den aktuella CTM:ens rotations-och-skalningskomponent innan den aktuella CTM:ens translation läggs på ovanpå. Hoppa över det steget och hårdkoda en naiv komponentvis kombination istället, och den första isolerade cm du testar kommer se korrekt ut medan varje koordinat nedströms om en andra eller tredje kapslad cm tyst driver iväg, vilket är precis den typen av bugg som överlever kodgranskning eftersom enhetstestet som skulle fånga det behöver minst två kedjade transformer för att misslyckas
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;
Loopen ovan svarar på smärtpunkten från inledningen: TPDFContentProgram.GetTextRuns ger tillbaka OriginX och OriginY redan vikta genom Tm, Td, TD, och T*, och TraceGraphicsStates(nil, False) tillhandahåller den enda återstående biten, den före-instruktion-CTM vid det exakta index varje körning fångades vid, i en enda linjär passering över hela programmet. Att skicka nil låter metoden äga en privat spårare för anropet och frigöra den internt, vilket är rätt val för en engångsskanning; att skicka en befintlig TPDFContentStateTracker-instans istället är vad som håller tillstånd kontinuerligt över en sida sammansatt från mer än en innehållsström, eftersom ISO 32000-1 behandlar en sidas /Contents-array som en logisk ström och q/Q-stacken måste stämma överens
Textmatrisen överlever Q; grafiktillståndet gör inte det
ISO 32000-1 §9.4.2 definierar Td, TD, Tm, och T* som operatorerna som bygger textmatrisen och textradmatrisen inuti ett BT/ET-block, och PDFlibPas håller den distinktionen skarp: Td och TD sammanfogar en ren translation på textradmatrisen, T* gör samma sak med den negativa av den aktuella ledningen (leading), och bara Tm ersätter båda matriserna helt med de sex talen den ges. BT återställer båda matriserna till identiteten, exakt en gång, i början av textobjektet — men q och Q rör dem inte alls. TPDFContentStateTracker.Apply specialbehandlar coRestoreState av precis den anledningen: innan den poppar det sparade tillståndet från stacken fångar den den aktuella textmatrisen, textradmatrisen, och BT/ET-flaggan, och tillämpar dem igen ovanpå vad det poppade tillståndet än råkade hålla, eftersom ett q/Q-par lindat runt en textkörning inte förväntas flytta textpositionen tillbaka
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 sekvensen och CTM:en rapporterad vid det andra Tj är tillbaka till identitetsskalan den hade innan q — 2 0 0 2 0 0 cm inuti spara/återställ-paret är borta, som q/Q kräver. TextMatrix.DX vid samma instruktion är dock fortfarande 100: Td som satte den kördes innan q, så det är inte grafiktillstånd Q någonsin var berättigad att röra, och ett verktyg som antog motsatsen skulle rapportera den andra glyfkörningen som startande från fel horisontell position på sidan
Vad händer när en klippbaningsoperator körs?
En W- eller W*-operator krymper inte klippet omedelbart; den registrerar bara vilken fyllningsregel som ska användas, och den faktiska genomskärningen väntar på vilken baningsmålningsoperator som än följer den, inklusive no-op-målaren n som PDF-författare rutinmässigt använder just för att klippa utan att rita något. TPDFContentStateTracker speglar den tvåstegs-timingen exakt: coClip och coClipEvenOdd sätter bara en väntande klippregel-flagga, och EndCurrentPath — anropad av varje baningsmålningsoperator — är vad som faktiskt skär in den väntande baningens gränser i ClipMinX, ClipMinY, ClipMaxX, och ClipMaxY. Att få den här stegningen rätt spelar roll för själva före/efter-ögonblicksbildskontraktet: en före-ögonblicksbild tagen exakt vid W-instruktionen måste fortfarande visa det gamla, bredare klippet, eftersom klippet ännu inte trätt i kraft vid den punkten i strömmen, och att slå ihop de två stegen till ett skulle tyst bryta varje anropare som förlitar sig på att före-tillstånd betyder vad det säger
ClipBoundsExact talar om för en anropare vilken av två situationer den tittar på, och den är bara någonsin True för en enda axeljusterad rektangel byggd av re på en annars tom baning — den enda formen PDFlibPas kan representera exakt som fyra tal. Allt annat — en roterad rektangel, en kurvad kontur, en sammansatt baning med flera underbaningar, eller ett klipp byggt från ett textrendreringsläge — producerar fortfarande ClipMinX till ClipMaxY, men med ClipBoundsExact rensad till False, en ärlig signal om att de fyra talen är en säker yttre gräns och inte den sanna klippformen; anropare som bara behöver den gränsen, som att isolera en rektangulär underregion innan GDI-halvtons-nedkonverteringen som beskrivs i att rendrera PDF-sidor till 1-bitars svartvitt, kan läsa den direkt istället för att härleda den på nytt från sidgeometrin
Bézier-kurvor: en exakt gräns eller en säker en
Det billigaste sättet att avgränsa ett kubiskt Bézier-segment är att ta det konvexa höljet av dess fyra kontrollpunkter, och det är alltid säkert eftersom kurvan aldrig lämnar det — men en grund, bred kurva kan rapportera en avgränsningsruta mycket större än vad kurvan faktiskt upptar, vilket försvagar klippbaserad filtrering precis när det spelar som mest roll, på stora dekorativa banor. PDFlibPas löser det snävare problemet istället: för varje axel löser den kubikkurvans derivata för rötter inuti det öppna intervallet (0, 1) och utvärderar kurvan vid eventuella rötter den hittar, tillsammans med båda ändpunkterna, vilket är det standardiserade slutna-formen-sättet att få en kurvas sanna axeljusterade utsträckning snarare än en överskattning. Precision per kurva förs dock inte över till själva klippet: när en kurvad kontur väl blir en klippbana faller ClipBoundsExact fortfarande till False för den, eftersom en avgränsningsruta, hur snäv den än är, fortfarande inte är samma form som kurvan den avgränsar, och tillståndsspåraren hellre säger det än låter en anropare anta en rektangel där en kurva faktiskt finns
Att läsa tillstånd före och efter varje operator
Huruvida en anropare vill ha före- eller efter-tillståndet beror helt på vad operatorn gör: en ritnings- eller träffprövningsfråga om en bana eller textkörning vill ha tillståndet som det stod ögonblicket innan den operatorn kördes, eftersom det är vad som faktiskt avgjorde hur operatorn målade, medan en diagnostisk fråga om en tillståndssättande operator som gs vanligtvis vill se vad den just ändrade. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) exponerar precis det valet som en enda boolesk, och beräknar ett TPDFContentGraphicsState per instruktion i en enda linjär passering över hela programmet oavsett vilket ögonblick som begärs. GetGraphicsState(InstructionIndex, AfterInstruction, State) erbjuder samma före/efter-val för en enda instruktion istället för hela programmet, men den spelar upp från instruktion noll vid varje anrop för att komma dit, så att skanna många index genom att anropa den i en loop kostar O(n²) mot ett enda O(n)-anrop till TraceGraphicsStates över samma 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;
Att leva med felformade innehållsströmmar
Två typer av felformad indata är vanliga nog i riktiga PDF-producenter att TPDFContentStateTracker måste tolerera dem snarare än att misslyckas på dem. Den första är en bana som sträcker sig över en q/Q-gräns: den aktuella banan, den aktuella punkten, och underbaningsantalet är inte grafiktillståndsparametrar — ISO 32000-1 §8.4 täcker vad q och Q sparar och återställer, och banan som just byggs är inte bland dem — så TPDFContentStateTracker spårar den datan helt utanför det sparade tillståndet, och en underbaning påbörjad innan en q finns fortfarande kvar, omålad, omedelbart efter matchande Q. Den andra är en bar Q utan någon matchande q någonstans innan den i strömmen, inte ovanligt i utdata från generatorer som sätter ihop innehållsströmsfragment genom sammanfogning och får bokföringen fel. TPDFContentStateTracker.RestoreUnderflowCount räknar varje sådan händelse istället för att kasta ett undantag eller korrumpera tillstånd: en omatchad Q lämnar bara det aktuella grafiktillståndet exakt som det var, som om den instruktionen hade varit en no-op, så resten av strömmen fortsätter spelas upp på ett sunt tillstånd och en anropare kan fortfarande bestämma i efterhand, från räkningen, om indatan är värd att flagga tillbaka till den som producerade den
CTM-sammansättning, textmatrisens oberoende från q/Q, och den stegvisa förverkligandet av en klippbana beror inte på hur eller om innehållsströmmen någonsin målas, vilket är precis poängen: samma TPDFContentStateTracker-ögonblicksbild är korrekt oavsett om sidan aldrig rendreras alls eller är på väg att lämnas till vilken backend PDFlibPas än väljer för den filen, inklusive den körtidsmotorväxling som täcks i guiden till multi-motor-PDF-rendrering i PDFlibPas. Innehållsanalys, koordinatmappning, och redigeringsverktyg (redaction) kan alla köras helt på spårarens utdata, långt innan eller helt utan att någonsin be en rendrerare bli inblandad
Innehållsströmsuppspelning genom TPDFContentStateTracker är en del av det strukturerade innehållsredigeringsramverket inbyggt i PDFlibPas, det nativa VCL PDF-komponentbiblioteket för Delphi och C++Builder