PDFlibPas, la libreria componente PDF VCL nativa per Delphi e C++Builder, riproduce il content stream di una pagina attraverso la propria classe TPDFContentStateTracker senza toccare affatto un canvas di rendering. Alimentare il tracker un operatore analizzato alla volta mantiene un registro di stato grafico in corso — matrice di trasformazione corrente, matrice di testo, confini di clip, e lo stack di salvataggio q/Q — disponibile per uno snapshot prima o dopo l'esecuzione di ogni operatore
Chiedi dove atterri realmente sulla pagina stampata un run di testo, e i numeri grezzi del content stream da soli ti trarranno in inganno ogni volta. TPDFContentProgram.GetTextRuns restituisce già il punto di ancoraggio di ogni istruzione di visualizzazione testo tramite i campi OriginX e OriginY su TPDFTextRun, e i commenti dei campi sono espliciti nel dire che questo punto risiede nello spazio testo, già ripiegato attraverso Tm, Td, TD e T*. Ciò che manca ancora, e ciò che quei commenti dicono che un chiamante deve fornire, è la CTM attiva a quella precisa istruzione — il prodotto di ogni cm concatenato finora, annidato dentro quante coppie q/Q capitino di essere aperte in quel punto dello stream
Perché riprodurre un content stream invece di renderizzarlo?
PDFlibPas mantiene due nozioni separate di stato grafico per due compiti separati, e la separazione è deliberata. Il registro di stato interno del renderer porta un handle di canvas device vivo, un handle di regione di clip, e cache di rasterizzazione font — risorse reali legate a qualunque superficie stia attualmente venendo dipinta, e senza significato una volta che quella superficie scompare. TPDFContentGraphicsState non porta nulla di ciò: è un semplice record limitato ai valori che ISO 32000-1 §8.4 definisce come raggiungibili dai soli operatori del content stream — la CTM, lo stile di linea, il colore, lo stato di testo, e i confini derivati di clip e percorso. Poiché il record non porta alcun riferimento a canvas e nessun handle di file aperto, un chiamante può analizzare un content stream, percorrerlo con TPDFContentStateTracker, e continuare a usare gli snapshot risultanti molto dopo che qualunque cosa abbia prodotto i byte sia scomparsa
Come costruisce la CTM TPDFContentStateTracker
TPDFContentStateTracker.Apply concatena i sei operandi di un operatore cm nella CTM del tracker usando la stessa pre-moltiplicazione che il PDF stesso specifica: la nuova matrice M2 si combina con la CTM corrente come M2 × CTM, nella convenzione a vettore riga dove un punto si trasforma come P′ = P × M (ISO 32000-1 §8.4). La parte facile da sbagliare risiede nel termine di traslazione, non nella parte lineare: la traslazione propria di M2 deve passare attraverso il componente di rotazione-e-scala della CTM corrente prima che la traslazione della CTM corrente venga aggiunta sopra. Salta quel passaggio e codifica invece a mano una combinazione ingenua componente per componente, e il primo cm isolato che testi sembrerà corretto mentre ogni coordinata a valle di un secondo o terzo cm annidato deriverà silenziosamente, il che è esattamente il tipo di bug che sopravvive alla revisione del codice perché lo unit test che lo catturerebbe ha bisogno di almeno due trasformazioni concatenate per fallire
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;
Il ciclo sopra risponde al punto dolente dell'apertura: TPDFContentProgram.GetTextRuns restituisce OriginX e OriginY già ripiegati attraverso Tm, Td, TD e T*, e TraceGraphicsStates(nil, False) fornisce l'unico pezzo rimanente, la CTM pre-istruzione all'esatto indice a cui ogni run è stato catturato, in un unico passaggio lineare sull'intero programma. Passare nil lascia che il metodo possieda un tracker privato per la chiamata e lo liberi internamente, la scelta giusta per una scansione una tantum; passare invece un'istanza TPDFContentStateTracker esistente è ciò che mantiene lo stato continuo attraverso una pagina assemblata da più di un content stream, poiché ISO 32000-1 tratta l'array /Contents di una pagina come un unico stream logico e lo stack q/Q deve concordare
La matrice di testo sopravvive a Q; lo stato grafico no
ISO 32000-1 §9.4.2 definisce Td, TD, Tm e T* come gli operatori che costruiscono la matrice di testo e la matrice di linea di testo dentro un blocco BT/ET, e PDFlibPas mantiene netta quella distinzione: Td e TD concatenano una pura traslazione sulla matrice di linea di testo, T* fa lo stesso usando il negativo del leading corrente, e solo Tm sostituisce entrambe le matrici del tutto con i sei numeri che riceve. BT azzera entrambe le matrici all'identità, esattamente una volta, all'inizio dell'oggetto testo — ma q e Q non le toccano affatto. TPDFContentStateTracker.Apply tratta coRestoreState come caso speciale proprio per questo motivo: prima di estrarre lo stato salvato dallo stack, cattura la matrice di testo corrente, la matrice di linea di testo, e il flag BT/ET, e li riapplica sopra a qualunque cosa lo stato estratto capitasse di contenere, perché una coppia q/Q avvolta attorno a un run di testo non deve spostare indietro la posizione del testo
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;
Esegui quella sequenza e la CTM segnalata al secondo Tj torna alla scala identità che aveva prima di q — il 2 0 0 2 0 0 cm dentro la coppia salva/ripristina è scomparso, come richiede q/Q. TextMatrix.DX in quella stessa istruzione, tuttavia, è ancora 100: il Td che lo ha impostato è girato prima del q, quindi non è stato di grafico su cui il Q avesse mai diritto di intervenire, e uno strumento che presupponesse il contrario segnalerebbe il secondo run di glifi come se iniziasse dalla posizione orizzontale sbagliata sulla pagina
Cosa succede quando gira un operatore di percorso di clip?
Un operatore W o W* non restringe immediatamente il clip; registra solo quale regola di riempimento usare, e la vera intersezione attende qualunque operatore di pittura del percorso che lo segua, incluso il pittore no-op n che gli autori PDF usano abitualmente proprio per fare clip senza disegnare nulla. TPDFContentStateTracker rispecchia precisamente quella tempistica in due fasi: coClip e coClipEvenOdd impostano solo un flag di regola di clip in attesa, e EndCurrentPath — invocato da ogni operatore di pittura del percorso — è ciò che effettivamente interseca i confini del percorso in attesa in ClipMinX, ClipMinY, ClipMaxX e ClipMaxY. Ottenere questa scansione corretta conta per il contratto stesso dello snapshot prima/dopo: uno snapshot-prima catturato esattamente all'istruzione W deve comunque mostrare il clip vecchio e più ampio, perché il clip non ha ancora effetto in quel punto dello stream, e collassare i due passaggi in uno solo romperebbe silenziosamente ogni chiamante che si affida allo stato-prima per significare ciò che dice
ClipBoundsExact dice a un chiamante quale delle due situazioni sta osservando, ed è vero solo per un singolo rettangolo allineato agli assi costruito da re su un percorso altrimenti vuoto — l'unica forma che PDFlibPas può rappresentare esattamente come quattro numeri. Tutto il resto — un rettangolo ruotato, un contorno curvo, un percorso composto con diverse sottotracce, o un clip costruito da una modalità di rendering testo — produce comunque ClipMinX fino a ClipMaxY, ma con ClipBoundsExact azzerato a False, un segnale onesto che i quattro numeri sono un confine esterno sicuro e non la vera forma di clip; i chiamanti che hanno bisogno solo di quel confine, come isolare una sotto-regione rettangolare prima della down-conversion a mezzatinta GDI descritta in renderizzare pagine PDF in monocromatico a 1 bit, possono leggerlo direttamente invece di ricavarlo dalla geometria di pagina
Curve di Bézier: un confine esatto o uno sicuro
Il modo più economico per delimitare un segmento cubico di Bézier è prendere l'inviluppo convesso dei suoi quattro punti di controllo, ed è sempre sicuro perché la curva non lo lascia mai — ma una curva bassa e larga può segnalare un riquadro delimitatore molto più grande di quanto la curva effettivamente occupi, il che indebolisce il filtraggio basato su clip proprio quando conta di più, su grandi percorsi decorativi. PDFlibPas risolve invece il problema più stretto: per ciascun asse, risolve la derivata della curva cubica per radici dentro l'intervallo aperto (0, 1) e valuta la curva a qualsiasi radice trovata, insieme a entrambi gli estremi, che è il modo standard in forma chiusa per ottenere la vera estensione allineata agli assi di una curva invece di una sovrastima. La precisione per-curva non si trasferisce però al clip stesso: una volta che un contorno curvo diventa un percorso di clip, ClipBoundsExact scende comunque a False per esso, perché un riquadro delimitatore, per quanto stretto, non è comunque la stessa forma della curva che delimita, e il tracker di stato preferisce dirlo piuttosto che lasciare che un chiamante presupponga un rettangolo dove in realtà c'è una curva
Leggere lo stato prima e dopo ogni operatore
Se un chiamante vuole lo stato prima o dopo dipende interamente da cosa fa l'operatore: una domanda di disegno o hit-testing su un percorso o run di testo vuole lo stato così com'era l'istante prima che quell'operatore girasse, poiché è quello che ha effettivamente determinato come l'operatore ha dipinto, mentre una domanda diagnostica su un operatore che imposta stato come gs solitamente vuole vedere cosa ha appena cambiato. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) espone esattamente quella scelta come un singolo Booleano, calcolando un TPDFContentGraphicsState per istruzione in un unico passaggio lineare sull'intero programma indipendentemente da quale istante venga richiesto. GetGraphicsState(InstructionIndex, AfterInstruction, State) offre la stessa scelta prima/dopo per una singola istruzione invece che per l'intero programma, ma riproduce dall'istruzione zero a ogni chiamata per arrivarci, quindi scansionare molti indici chiamandolo in un ciclo costa O(n²) contro un'unica chiamata O(n) a TraceGraphicsStates sullo stesso programma
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;
Convivere con content stream malformati
Due tipi di input malformato sono abbastanza comuni in veri produttori PDF da far sì che TPDFContentStateTracker debba tollerarli invece di fallire su di essi. Il primo è un percorso che attraversa un confine q/Q: il percorso corrente, il punto corrente, e il conteggio delle sottotracce non sono parametri di stato grafico — ISO 32000-1 §8.4 copre cosa q e Q salvano e ripristinano, e il percorso corrente in costruzione non è tra questi — quindi TPDFContentStateTracker traccia quel dato interamente al di fuori dello stato salvato, e una sottotraccia iniziata prima di un q è ancora lì, non dipinta, immediatamente dopo il Q corrispondente. Il secondo è un Q nudo senza alcun q corrispondente in precedenza nello stream, non raro nell'output di generatori che assemblano frammenti di content stream per concatenazione e sbagliano la contabilità. TPDFContentStateTracker.RestoreUnderflowCount conta ognuno di quegli eventi invece di sollevare un'eccezione o corrompere lo stato: un Q senza corrispondenza lascia semplicemente lo stato grafico corrente esattamente com'era, come se quell'istruzione fosse stata un no-op, cosicché il resto dello stream continui a riprodursi su uno stato sano e un chiamante possa comunque decidere in seguito, dal conteggio, se l'input meriti di essere segnalato a chi lo ha prodotto
La composizione della CTM, l'indipendenza della matrice di testo da q/Q, e la realizzazione a fasi di un percorso di clip non dipendono da come o se il content stream venga mai dipinto, ed è esattamente questo il punto: lo stesso snapshot di TPDFContentStateTracker è corretto sia che la pagina non venga mai renderizzata affatto sia che stia per essere passata a qualunque back end PDFlibPas selezioni per quel file, incluso il cambio di motore a runtime trattato nella guida al rendering PDF multi-motore in PDFlibPas. L'analisi del contenuto, la mappatura delle coordinate, e gli strumenti di redazione possono girare tutti interamente sull'output del tracker, molto prima o completamente senza mai chiedere a un renderer di intervenire
La riproduzione del content stream tramite TPDFContentStateTracker fa parte del framework di editing di contenuto strutturato integrato in PDFlibPas, la libreria componente PDF VCL nativa per Delphi e C++Builder