Page renderer komponentu HotPDF Delphi Component teraz posúva text počítaním každého posunutia glyfu v textovom priestore, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, ako to definuje ISO 32000-1 §9.4.4, a potom posúva textovú maticu cez jej lineárnu časť s HPDFTranslateTextMatrix. Orezávanie sa ukladá na každý rámec q a obnovuje na Q, ale GDI región sa zachytí len vtedy, keď ten rámec clip naozaj mení. Obe opravy pristáli v HotPDF 2.754.0 a obe prišli z reálnych strán renderovaných so zhluknutými slovami alebo s clip regiónmi presakujúcimi cez ich Q. Prvá chyba je aritmetika, ktorá vyzerá správne, kým producent nezapíše veľkosť fontu do matice. Druhá je oprava korektnosti, ktorá takmer stála paralelné zrýchlenie renderu, a spôsob, akým sme rýchlosť dostali späť, stojí za poznanie, keď píšete akékoľvek GDI-backed PDF zariadenie
Prečo sa text zhlukne, keď PDF používa Tf 1?
Pretože starý advance kód pričítal textovopriestorovú vzdialenosť rovno k translačnej zložke Tm, akoby textový priestor a používateľský priestor vždy mali rovnakú mierku. Plno reálnych producentov nastaví veľkosť fontu na 1 cez Tf a nesie skutočnú veľkosť v textovej matici. S /F1 1 Tf a 12 0 0 12 72 700 Tm posunie glyf široký 500 jednotiek 0,5 v textovom priestore, čo je 6 bodov na strane, keď ho Tm škáluje. Starý renderer vykonal Tm.e := Tm.e + Adv a pohnul perom o 0,5 bodu. Každý glyf pristál jedna dvanástina znaku za predchádzajúcim, takže riadok textu sa vyrenderoval ako tmavá škvrna pri ľavom okraji, kým ten istý súbor vyzeral perfektne v každom inom prehliadači
// Content stream od producenta kódujúceho veľkosť v Tm, nie Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Starý advance (zjednodušené): vzdialenosť pripočítaná do Tm.e, akoby to bol user space
Adv := W * FontSize / 1000; // 0,5 pre glyf o 500 jednotkách
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th len na šírku
Adv := Adv + CharSpace; // Tc neškálované Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw zle škálované Tfs
Tm.e := Tm.e + Adv; // ignoruje Tm.a, Tm.b, Tm.c, Tm.d
// Stará úprava TJ: žiadne Th a znova len Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
Skratka Tm.e nebola jediný defekt v tom bloku. Medzislovná medzera Tw sa vyjadruje v neškálovaných jednotkách textového priestoru, aj tak starý kód násobil FontSize / 1000, takže pri Tf 12 riadok zarovnaný do bloku stratil takmer celú svoju medzislovnú medzeru. Horizontálne škálovanie Th sa uplatnilo na šírku glyfu, ale nie na Tc ani Tw a kerningová úprava TJ ho preskočila úplne. Nepaintujúca cesta posúvajúca neviditeľný text render režimu 3, druh, ktorý používajú OCR textové vrstvy, a text vnútri skrytého optional content niesli súkromnú kópiu tej istej aritmetiky, takže čokoľvek nakreslené po neviditeľnom behu štartovalo zo zlej pozície. Bugy textového stavu v rendereri zriedka zlyhajú nahlas: ako bugy operand indexu a mena zdroja, ktoré raz vynulovali Tc, Tw a Tz bez jedinej chyby, vyrábali vierohodné strany na vlastnom výstupe knižnice a rozbili sa len na súboroch od iných producentov
Ako ISO 32000-1 §9.4.4 definuje advance glyfu?
ISO 32000-1 §9.4.4 definuje advance úplne v textovom priestore a aplikuje ho na textovú maticu ako translačnú maticu, takže odpoveď je spočítať najprv tx a nechať Tm robiť škálovanie, rotáciu a skew. Pre horizontálne písanie sa tx rovná ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, kde w0 je šírka glyfu v tisícinách em, Tj je úprava TJ a Th je Tz delené 100. Nové Tm je [1 0 0 1 tx 0] × Tm, čo je v HotPDF helper HPDFTranslateTextMatrix: pričíta X a Y cez maticové koeficienty a, b, c a d namiesto priameho zápisu do e a f. Podľa §9.3.3 sa Tw uplatňuje len na jednobajtový kód znaku 32, takže viacbajtové CID kódy nikdy nezoberú medzislovnú medzeru na horizontálnej ceste. Ten istý helper teraz riadi Td, TD, T*, operátory ' a ", úpravy TJ aj skrytú textovú cestu, čo znamená, že jedna funkcia vlastní pravidlo
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;
// Horizontálny advance glyfu, 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 v textovom priestore, neškálované
Adv := Adv * State.Text.HorizScale / 100; // Th sa uplatní na celý súčet
HPDFTranslateTextMatrix(Tm, Adv, 0);
// Číselný element TJ: ten istý priestor, to isté Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Umiestňovanie glyfov muselo nasledovať tú istú logiku. Keď nie je k dispozícii vložený obrys a renderer spadne na GDI TextOutW, teraz postaví plnú maticu glyfu z CTM × Tm × rise × em scale, vrátane Th, a nainštaluje ju cez SetWorldTransform v režime GM_ADVANCED vnútri páru SaveDC / RestoreDC. GDI font sa vytvára na fixnej výške 1000 jednotiek a transformácia robí veľkosť, takže otočený a skosený text drží orientáciu namiesto vykreslenia vzpriamene na transformovanom pôvodnom bode. Vertikálny režim písania je tá jediná zámerná asymetria: font WMode 1 posúva y osou nadol po svojej vertikálnej metrike a horizontálne škálovanie sa na tú os nevzťahuje
Čo q/Q vlastne ukladá v grafickom stave PDF?
ISO 32000-1 §8.4.2 radí aktuálnu orezávaciu cestu medzi časti grafického stavu, takže Q musí obnoviť clip presne taký, aký bol pri zodpovedajúcom q, nie len číselné parametre. HotPDF už držal stack grafického stavu s CTM, farbami, parametrami čiar a textovým stavom, ale GDI drží clip v device contexte, mimo toho stacku. Kópia číselného stavu preto obnovila všetko okrem clipu a clip nainštalovaný cez W n vnútri bloku q ... Q pokračoval v orezávaní každej neskoršej operácie na strane. Form XObjecty pridali druhú cestu k tomu istému zlyhaniu, lebo §8.10 dáva forme implicitné save a restore okolo jej obsahu a reálny obsah formulárov niekedy necháva vlastné operátory q nevyrovnané, aj keď špecifikácia vyžaduje ich párovanie. Renderer teraz volá CaptureClipBeforeChange a SaveDC pred spustením formy a potom, čo forma skončí, zahodí všetky uložené regióny hlbšie než vstupná hĺbka a zavolá RestoreDC, takže každý uložený HRGN má presne jednu cestu uvoľnenia
Lenivé zachytávanie clipu s THPDFSavedClipState
Oprava, ktorá odišla von, ukladá jeden záznam THPDFSavedClipState na q, ale odkladá drahú časť, kým rámec prvýkrát nemení clip. Záznam drží handle regiónu, hĺbku stacku, do ktorej patrí, device context, z ktorého sa vzal, a flag Captured. DevPushState len doplní hĺbku a DC a zväčší pole rámcov zdvojnásobovaním od 16, takže content stream plný q 1 0 0 1 x y cm ... Q nealokuje vôbec žiadny GDI objekt. Operátory, ktoré sa chystajú meniť orezanie, teda path painting s čakajúcim W alebo W*, operátor n, pattern fill a vstup do formy, volajú najprv 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; // už uložené, alebo nie naše
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 znamená vôbec žiadny 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); // Región 0 odstráni clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
Nameraná cena zapálenej verzie je dôvod, prečo tento dizajn existuje. Prvá korektná implementácia vytvárala a čítala GDI región na každom q a na stranách zložených prevažne z číselných transformácií trávili render vlákna čas súperením o GDI región objekty namiesto rasterizovania. Paralelná render pipeline padla z očakávaného zisku na zhruba 1,13 až 1,20 krát jednovláknovú priepustnosť a prepasla bránu zrýchlenia 1,5 krát v benchmark sade. S lenivým zachytávaním a znovu používanou kapacitou rámcov ten istý benchmark znovu prechádza pôvodnou bránou 1,5 krát. Antialiasing malých TrueType glyfov pristál v tom istom vydaní a bol zjavným podozrivým, ale regresia sa vystopovala späť k alokácii regiónov, čo je dobrá pripomienka merať skôr, než obviníte najnovšiu funkciu
Kde sú limity tohto prístupu?
Uložený clip je GDI región v device pixeloch, takže je presný pre bitmapu, ktorá sa renderuje, a bezvýznamný pre akýkoľvek iný cieľ. Práve preto každý rámec zaznamenáva svoj device context a DevPopState preskočí obnovu, keď sa DC zmenil, napríklad kým sa transparency group renderuje do vlastnej vrstvovej bitmapy. Nula vrátená z GetClipRgn je legitímny výsledok znamenajúci žiadny clip a jeho obnovenie cez SelectClipRgn(FDC, 0) je to, čo správne odstráni clip, ktorý pri zodpovedajúcom q neexistoval. Na textovej strane oprava opravuje, kam každý glyf ide, ale nevymýšľa šírky: ak font vynechá pole /Widths a vložený program nie je dostupný, advance je stále len taký dobrý, aký je width fallback. Keď túto oblasť regresne testujete, držte aspoň jeden fixture s Tf 1 a škálovaným Tm, jeden s nenulovým Tz a Tw a jeden s clipom vo vnútri q ... Q nasledovaným obsahom mimo neho, lebo žiadny z nich sa neukáže v dokumentoch generovaných samotnou knižnicou
Ak renderer riadite z aplikačného kódu, vo volacom vzore popísanom v článku o renderovaní PDF strany na bitmapu sa nič nemení a strany, ktoré predtým ukazovali rozmazané riadky alebo orezaný obsah, by mali na 2.754.0 a novších prosto renderovať správne. Detaily komponentu, podporované verzie Delphi a C++Builder a licencovanie sú na stránke produktu HotPDF Delphi PDF Component