Il renderer di pagine di HotPDF Delphi Component ora avanza il testo calcolando lo spostamento di ogni glyph in text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th come lo definisce ISO 32000-1 §9.4.4, e poi muovendo la matrice testo attraverso la sua parte lineare con HPDFTranslateTextMatrix. Il clipping viene salvato per ogni frame q e ripristinato su Q, ma una regione GDI viene catturata solo quando quel frame cambia davvero la clip. Entrambe le correzioni sono atterrate in HotPDF 2.754.0, e entrambe vengono da pagine reali che renderizzavano con parole accartocciate o regioni di clip che trapelavano oltre il loro Q. Il primo bug è aritmetica che sembra giusta finché un producer non scrive la dimensione del font nella matrice. Il secondo è una correzione di correttezza che ha quasi fatto perdere l'accelerazione del rendering parallelo, e il modo in cui abbiamo ripreso la velocità vale la pena conoscerlo se scrivi un qualunque dispositivo PDF appoggiato a GDI
Perché il testo si accartoccia in un grumo quando un PDF usa Tf 1?
Perché il vecchio codice di avanzamento aggiungeva una distanza in text space direttamente alla componente di traslazione di Tm, come se text space e user space avessero sempre la stessa scala. Un sacco di producer reali impostano la dimensione del font a 1 con Tf e portano la dimensione vera nella matrice testo. Con /F1 1 Tf e 12 0 0 12 72 700 Tm, un glyph largo 500 unità avanza di 0.5 in text space, che sono 6 punti sulla pagina una volta che Tm lo scala. Il vecchio renderer eseguiva Tm.e := Tm.e + Adv e muoveva la penna di 0.5 punti. Ogni glyph atterrava un dodicesimo di carattere dopo il precedente, così una riga di corpo testo si renderizzava come una macchia scura al margine sinistro mentre lo stesso file appariva perfetto in ogni altro viewer
// Content stream da un producer che codifica la dimensione in Tm, non in Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Vecchio avanzamento (semplificato): distanza aggiunta a Tm.e come se fosse user space
Adv := W * FontSize / 1000; // 0.5 per un glyph da 500 unità
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th solo sulla larghezza
Adv := Adv + CharSpace; // Tc non scalato da Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw scalato per errore da Tfs
Tm.e := Tm.e + Adv; // ignora Tm.a, Tm.b, Tm.c, Tm.d
// Vecchia correzione TJ: niente Th, e di nuovo solo Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
La scorciatoia Tm.e non era l'unico difetto in quel blocco. Lo word spacing Tw è espresso in unità di text space non scalate, eppure il vecchio codice lo moltiplicava per FontSize / 1000, quindi sotto Tf 12 una riga giustificata perdeva quasi tutto il suo spazio tra le parole. La scalatura orizzontale Th si applicava alla larghezza del glyph ma non a Tc o Tw, e la correzione di kerning TJ la saltava del tutto. Il percorso non pittorico che avanza il testo invisibile in render mode 3, il genere che usano i layer di testo OCR, e il testo dentro optional content nascosti portavano una copia privata della stessa aritmetica, quindi qualunque cosa disegnata dopo una corsa invisibile partiva dalla posizione sbagliata. I bug di stato testo in un renderer raramente falliscono rumorosamente: come i bug dell'indice degli operandi e dei nomi di risorsa che una volta azzerarono Tc, Tw e Tz senza un solo errore, questi producevano pagine plausibili sull'output della libreria stessa e si rompevano solo su file di altri producer
Come definisce l'avanzamento del glyph ISO 32000-1 §9.4.4?
ISO 32000-1 §9.4.4 definisce l'avanzamento interamente in text space e lo applica alla matrice testo come matrice di traslazione, quindi la risposta è calcolare prima tx e lasciare che sia Tm a fare scalatura, rotazione e skew. Per la scrittura orizzontale, tx eguaglia ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, dove w0 è la larghezza del glyph in millesimi di em, Tj è la correzione TJ, e Th è Tz diviso 100. La nuova Tm è [1 0 0 1 tx 0] × Tm, che in HotPDF è l'helper HPDFTranslateTextMatrix: somma X e Y attraverso i coefficienti di matrice a, b, c e d invece di scrivere direttamente su e e f. Per §9.3.3, Tw si applica solo al codice di carattere a byte singolo 32, quindi i codici CID multibyte non raccolgono mai word spacing sul percorso orizzontale. Lo stesso helper ora pilota Td, TD, T*, gli operatori ' e ", le correzioni TJ e il percorso del testo nascosto, il che significa che una sola funzione possiede la regola
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;
// Avanzamento orizzontale del glyph, ISO 32000-1 9.4.4
W := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
Adv := Adv + State.Text.WordSpace; // Tw in text space, non scalato
Adv := Adv * State.Text.HorizScale / 100; // Th si applica all'intera somma
HPDFTranslateTextMatrix(Tm, Adv, 0);
// Elemento numerico TJ: stesso spazio, stesso Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Il piazzamento dei glyph ha dovuto seguire la stessa logica. Quando non è disponibile alcun contorno incluso e il renderer ripiega su GDI TextOutW, ora costruisce la matrice completa del glyph da CTM × Tm × rise × scala em, Th inclusa, e la installa con SetWorldTransform in modalità GM_ADVANCED dentro una coppia SaveDC / RestoreDC. Il font GDI viene creato a un'altezza fissa di 1000 unità e la trasformazione fa il ridimensionamento, così il testo ruotato e inclinato mantiene il suo orientamento invece di essere disegnato dritto in un punto di origine trasformato. La scrittura verticale è l'unica asimmetria deliberata: un font WMode 1 avanza lungo l'asse y della sua metrica verticale, e la scalatura orizzontale non si applica a quell'asse
Che cosa salva davvero q/Q nello stato grafico di un PDF?
ISO 32000-1 §8.4.2 elenca il clipping path corrente come parte dello stato grafico, quindi Q deve ripristinare la clip esattamente com'era al q corrispondente, non solo i parametri numerici. HotPDF teneva già uno stack di stato grafico con CTM, colori, parametri di linea e stato testo, ma GDI tiene la clip nel device context, fuori da quello stack. Una copia dello stato numerico ripristinava dunque tutto tranne la clip, e una clip installata con W n dentro un blocco q ... Q continuava a ritagliare ogni operazione successiva sulla pagina. I Form XObjects aggiungevano una seconda strada verso lo stesso fallimento, perché §8.10 dà a una form un save e restore impliciti attorno al suo contenuto, e il contenuto delle form reali ogni tanto lascia i propri operatori q sbilanciati pur richiedendo la specifica che si accoppino. Il renderer ora chiama CaptureClipBeforeChange e SaveDC prima di eseguire una form, poi dopo che la form ha finito scarta le regioni salvate più profonde della profondità di ingresso e chiama RestoreDC, così ogni HRGN salvata ha esattamente un percorso di rilascio
Cattura lazy della clip con THPDFSavedClipState
La correzione spedita salva un record THPDFSavedClipState per ogni q, ma rinvia la parte costante finché il frame non modifica per la prima volta la clip. Il record tiene l'handle della regione, la profondità di stack a cui appartiene, il device context da cui è stata presa e un flag Captured. DevPushState riempie solo profondità e DC e fa crescere l'array dei frame raddoppiando da 16, così un content stream pieno di q 1 0 0 1 x y cm ... Q non alloca nessun oggetto GDI. Gli operatori che stanno per cambiare il clipping, cioè la pittura di un percorso con un W o W* in sospeso, l'operatore n, i fill a pattern e l'ingresso in form, chiamano prima CaptureClipBeforeChange
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
Index, ClipResult: Integer;
Region: HRGN;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index < 0) or FSavedClips[Index].Captured or
(FSavedClips[Index].StackDepth <> FGSStack.Count) or
(FSavedClips[Index].DC <> FDC) then Exit; // già salvato, o non nostro
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 significa nessuna clip
if ClipResult <= 0 then
begin
DeleteObject(Region);
Region := 0;
if ClipResult < 0 then RaiseLastOSError;
end;
FSavedClips[Index].Region := Region;
FSavedClips[Index].Captured := True;
end;
procedure THPDFPageRenderer.DevPopState;
var
Index: Integer;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
begin
if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
SelectClipRgn(FDC, FSavedClips[Index].Region); // Region 0 rimuove la clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
Il costo misurato della versione eager è la ragione per cui questo progetto esiste. La prima implementazione corretta creava e leggeva una regione GDI a ogni q, e sulle pagine fatte per lo più di trasformazioni numeriche i thread del renderer passavano il tempo a contendersi gli oggetti regione GDI invece di rasterizzare. La pipeline di rendering parallela scendeva dal guadagno atteso a circa 1.13-1.20 volte il throughput single-threaded e falliva il gate di speedup 1.5 volte nella suite di benchmark. Con cattura lazy e capacità dei frame riutilizzata, lo stesso benchmark passa di nuovo il gate originale a 1.5 volte. La piccola antialiasing dei glyph TrueType atterrata nella stessa release era il sospetto ovvio, ma la regression risaliva all'allocazione delle regioni, che è un buon promemoria per misurare prima di incolpare la feature più recente
Dove stanno i limiti di questo approccio?
La clip salvata è una regione GDI in pixel di dispositivo, quindi è esatta per la bitmap che viene renderizzata e senza senso per qualunque altro target. Ecco perché ogni frame registra il suo device context e DevPopState salta il ripristino quando il DC è cambiato, per esempio mentre un transparency group renderizza nella sua bitmap di layer. Un GetClipRgn che restituisce zero è un risultato legittimo che significa nessuna clip, e ripristinarlo con SelectClipRgn(FDC, 0) è ciò che rimuove correttamente una clip che non esisteva al q corrispondente. Sul lato testo, la correzione sistemata dove va ogni glyph, ma non inventa le larghezze: se un font omette il suo array /Widths e il programma incluso non è disponibile, l'avanzamento resta comunque buono solo quanto il fallback delle larghezze. Quando fai regression su quest'area, tieni almeno una fixture con Tf 1 e una Tm scalata, una con Tz e Tw non nulli, e una con una clip dentro q ... Q seguita da contenuto fuori, perché nessuna di queste compare nei documenti generati dalla libreria stessa
Se piloti il renderer da codice applicativo, nel pattern di chiamata descritto in renderizzare una pagina PDF in una bitmap non cambia nulla, e le pagine che prima mostravano righe sbavate o contenuti ritagliati dovrebbero semplicemente renderizzarsi correttamente sulla 2.754.0 e successive. Dettagli sul componente, versioni Delphi e C++Builder supportate e licenze sono sulla pagina di prodotto di HotPDF Delphi PDF Component