HotPDF Delphi Component puslapių atvaizduoklis dabar vardo tekstą skaičiuodamas kiekvieno glifo poslinkį teksto erdvėje – tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, kaip apibrėžia ISO 32000-1 §9.4.4 – o tada perkelia teksto matricą pro jos tiesinę dalį su HPDFTranslateTextMatrix. Iškirpimas išsaugomas kiekvienam q kadrui ir atstatomas ties Q, bet GDI regionas užfiksuojamas tik tada, kai tas kadras iš tikrųjų keičia clip. Abi pataisos atkeliavo į HotPDF 2.754.0, ir abi kilo iš tikro pasaulio puslapių, atvaizduotų su susiglaudusiais žodžiais arba clip regionais, nutekančiais pro savą Q. Pirmasis bugas yra aritmetika, kuri atrodo teisinga, kol generatorius įrašo savo šrifto dydį į matricą. Antra yra teisingumo pataisa, kuri beveik kainavo mums lygiagretaus atvaizdavimo paspartėjimą, ir būdas, kuriuo greitį susigrąžinome, vertas žinoti, jei rašote bet kokį GDI paremtą PDF įrenginį
Kodėl tekstas susiglaudžia į gumulą, kai PDF naudoja Tf 1?
Nes senasis eigos kodas teksto erdvės atstumą pridėdavo tiesiai į Tm perstūmimo komponentę, tarsi teksto erdvė ir vartotojo erdvė visuomet turėtų tą patį mastelį. Nemačiai tikrų pasaulio generatorių nustato šrifto dydį į 1 su Tf ir tikrąjį dydį neša teksto matricoje. Su /F1 1 Tf ir 12 0 0 12 72 700 Tm 500 vienetų pločio glifas pasislenka 0.5 teksto erdvėje, o tai 6 taškai puslapyje, kai Tm jį perskaičiuoja. Senasis atvaizduoklis vykdydavo Tm.e := Tm.e + Adv ir pajudindavo pieštuką 0.5 taško. Kiekvienas glifas nusileisdavo dvyliktąja simbolio dalimi už ankstesniojo, tad pagrindinio teksto eilutė atvaizduodavo kaip tamsus dryžas prie kairiojo krašto, o tas pats failas kiekvienoje kitoje peržiūros programoje atrodė puikiai
// Turinio srautas iš generatoriaus, koduojančio dydį Tm, o ne Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Senoji eiga (supaprastinta): atstumas pridedamas prie Tm.e, tarsi tai būtų vartotojo erdvė
Adv := W * FontSize / 1000; // 0.5 už 500 vienetų glifą
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th tik pločiui
Adv := Adv + CharSpace; // Tc neskaluojama Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw neteisingai skaluojama Tfs
Tm.e := Tm.e + Adv; // ignoruoja Tm.a, Tm.b, Tm.c, Tm.d
// Senasis TJ derinimas: be Th, ir vėl tik Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
Tm.e trumpasis kelias nebuvo vienintelis to bloko defektas. Žodžių tarpas Tw išreiškiamas neskaluotais teksto erdvės vienetais, bet senasis kodas jį daugindavo iš FontSize / 1000, tad ties Tf 12 sulyginta eilutė prarasdavo beveik visą tarpžodinį tarpą. Horizontalioji skalė Th taikėsi glifo pločiui, bet ne Tc ar Tw, o TJ kerningo derinimas jos visai nepaisydavo. Nepiešiantis kelias, vardantis render režimo 3 nematomą tekstą – tokį, kokį naudoja OCR teksto sluoksniai – ir tekstą po paslėptu optional content turėjo privačią tos pačios aritmetikos kopiją, tad bet kas, nupiešta po nematoma eiga, startuodavo iš neteisingos pozicijos. Teksto būsenos bugai atvaizduoklyje retai žlunga garsiai: kaip ir operando indekso ir resursų vardo bugai, kartą nunulinę Tc, Tw ir Tz be nė vienos klaidos, jie duodavo tikėtinus puslapius ant pačios bibliotekos išvesties ir žlugdavo tik kitų generatorių failuose
Kaip ISO 32000-1 §9.4.4 apibrėžia glifo eigą?
ISO 32000-1 §9.4.4 eigą apibrėžia visiškai teksto erdvėje ir ją teksto matricai taiko kaip perstūmimo matricą, tad atsakymas – paskaičiuoti tx pirmiausia ir leisti Tm padaryti skalę, pasukimą ir iškraipymą. Horizontaliam rašymui tx lygus ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, kur w0 – glifo plotis tūkstantosiose em dalimis, Tj – TJ derinimas, o Th – Tz, padalintas iš 100. Nauja Tm yra [1 0 0 1 tx 0] × Tm, kas HotPDF yra pagalbinė HPDFTranslateTextMatrix: ji prideda X ir Y pro matricos koeficientus a, b, c ir d, vietoj to, kad rašytų tiesiai į e ir f. Pagal §9.3.3 Tw taikosi tik į vieno baito simbolio kodą 32, tad kelių baitų CID kodai niekada nepakelia žodžių tarpo horizontaliame kelyje. Tas pats pagalbininkas dabar valdo Td, TD, T*, ' ir " operatorius, TJ derinimus ir paslėpto teksto kelią, o tai reiškia, kad viena funkcija valdo taisyklę
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;
// Horizontali glifo eiga, 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 teksto erdvėje, neskaluota
Adv := Adv * State.Text.HorizScale / 100; // Th taikosi visai sumai
HPDFTranslateTextMatrix(Tm, Adv, 0);
// TJ skaitinis elementas: ta pati erdvė, tas pats Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Glifų išdėstymas turėjo sekti tą pačią logiką. Kai nėra įtaikyto kontūro ir atvaizduoklis grįžta prie GDI TextOutW, jis dabar pilną glifo matricą stato iš CTM × Tm × rise × em skalės, įskaitant Th, ir įdiegia ją SetWorldTransform GM_ADVANCED režime SaveDC / RestoreDC poros viduje. GDI šriftas kuriamas fiksuoto 1000 vienetų aukščio, o transformacija daro dydį, tad pasuktas ir iškreiptas tekstas išlaiko savo orientaciją, vietoj to, kad būtų piešiamas statmenai pakeistoje kilmės taško vietoje. Vertikalusis rašymo režimas yra vienintelė sąmoninga asimetrija: WMode 1 šriftas eina žemyn y ašimi savo vertikaliąja metrika, o horizontalioji skalė į tą ašį nesitaiko
Ką q/Q iš tikrųjų išsaugoja PDF grafikos būsenoje?
ISO 32000-1 §8.4.2 dabartinį iškirpimo kelią įrašo į grafikos būsenos sudėtį, tad Q privalo atstatyti clip lygiai tokį, koks jis buvo ties atitinkamu q, o ne tik skaitinius parametrus. HotPDF jau turėjo grafikos būsenos krūvą su CTM, spalvomis, linijos parametrais ir teksto būsena, bet GDI clip laiko įrenginio kontekste, už tos krūvos ribų. Skaitinės būsenos kopija todėl atstatydavo viską, išskyrus clip, ir clip, įdiegtas W n q ... Q bloko viduje, toliau kirpo kiekvieną vėlesnę puslapio operaciją. Form XObjects pridėjo antrą kelią į tą patį žlugimą, nes §8.10 formai duoda netiesioginį išsaugojimą ir atstatymą aplink jos turinį, o tikro pasaulio formų turinys kartais palieka savus q operatorius nesubalansuotus, nors specifikacija reikalauja juos suporuoti. Atvaizduoklis dabar kviečia CaptureClipBeforeChange ir SaveDC prieš paleisdamas formą, o formai pasibaigus išmeta bet kokius giliau įrašytus regionus už įėjimo gylio ir kviečia RestoreDC, tad kiekviena įrašyta HRGN turi lygiai vieną atlaisvinimo kelią
Tingus clip fiksavimas su THPDFSavedClipState
Išleista pataisa išsaugoja po vieną THPDFSavedClipState įrašą kiekvienam q, bet brangiąją dalį atideda, kol kadras pirmąkart pakeičia clip. Įrašas laiko regiono rankeną, krūvos gylį, kuriam priklauso, įrenginio kontekstą, iš kurio paimta, ir Captured vėliavėlę. DevPushState užpildo tik gylį ir DC bei augina kadrų masyvą dvigubindamas nuo 16, tad turinio srautas, pilnas q 1 0 0 1 x y cm ... Q, visai nealokuoja GDI objekto. Operatoriai, ketinantys keisti iškirpimą – tai kelio piešimas su laukiančiu W ar W*, n operatorius, raštų užpildymai ir formos įėjimas – pirmiausia kviečia 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; // jau įrašyta arba ne mūsų
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 reiškia iš viso jokio 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); // Regionas 0 pašalina clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
Išmatuota užuklusiosios versijos kaina yra priežastis, kodėl šis dizainas egzistuoja. Pirmasis teisingas variantas kūrė ir skaitė GDI regioną kiekvienam q, ir puslapiuose, daugiausia sudarytuose iš skaitinių transformacijų, atvaizdavimo gijos leisdavo laiką varžydamosi dėl GDI regiono objektų, vietoj to, kad rastrintų. Lygiagretus atvaizdavimo konvejeris nukrito nuo tikėtos laimės iki maždaug 1,13–1,20 karto vienagijio pralaidumo ir žlugo ant 1,5 karto paspartėjimo vartų testų rinkinyje. Su tingiu fiksavimu ir pakartotinai naudojama kadrų talpa tas pats testas vėl praeina originalius 1,5 karto vartus. Mažasis TrueType glifų antialiasing atkeliavo toje pačioje laidoje ir buvo akivaizdus įtariamasis, bet regresija išsitrakė iki regiono alokavimo – geras priminimas išmatuoti dar prieš kaltinant naujausią funkciją
Kur šio požiūrio ribos?
Įrašytas clip yra GDI regionas įrenginio pikseliais, tad jis tiksli atvaizduojamam bitmap ir beprismis bet kuriam kitam taikiniui. Todėl kiekvienas kadras užsirašo savo įrenginio kontekstą, o DevPopState praleidžia atstatymą, kai DC pasikeitė – pavyzdžiui, kol transparencijos grupė atvaizduoja į savą sluoksnio bitmap. GetClipRgn, grąžinanti nulį, yra teisėtas rezultatas, reiškiantis jokio clip, ir jo atstatymas su SelectClipRgn(FDC, 0) yra būtent tai, kas teisingai pašalina clip, kurio ties atitinkamu q nebuvo. Teksto pusėje pataisa ištaiso, kur kiekvienas glifas nukeliauja, bet neišgalvoja pločių: jei šriftas praleidžia savą /Widths masyvą, o įtaikytoji programa neprieinama, eiga vis tiek tokia gera, kokia gera pločių atsarginė išėja. Regresiškai tikrindami šią sritį, laikykite bent vieną fiksuotą pavyzdį su Tf 1 ir skalinta Tm, vieną su nenuliniu Tz ir Tw, ir vieną su clip q ... Q viduje, po kurio seka turinys už jo ribų, nes nė vienas tų atvejų nepasirodo pačios bibliotekos generuotuose dokumentuose
Jei atvaizduoklį valdote iš programos kodo, kvietimo rašte, aprašytame PDF puslapio atvaizdavime į bitmap, nesikeičia niekas, o puslapiai, anksčiau rodę išsitepusias eilutes arba apkarpytą turinį, tiesiog turėtų atvaizduotis teisingai 2.754.0 ir vėlesnėse versijose. Detalės apie komponentą, palaikomas Delphi ir C++Builder versijas bei licencijavimą – HotPDF Delphi PDF Component produkto puslapyje