Tehnički članak

PDF text advance i q/Q oporavak clipa u Delphi rendereru

Renderer stranica HotPDF Delphi Component sada pomiče tekst računajući svaki pomak glifa u text spaceu, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th kako ga definira ISO 32000-1 §9.4.4, pa zatim pomiče text matricu kroz njezin linearni dio s HPDFTranslateTextMatrix. Clipping se sprema po q okviru i obnavlja na Q, ali se GDI regija hvata samo kad taj okvir stvarno mijenja clip. Oba popravka sletjela su u HotPDF 2.754.0, i oba su došla sa stranica iz stvarnog svijeta koje su se renderirale sa zgužvanim riječima ili clip regijama koje su curile mimo svoga Q. Prvi bug aritmetika je koja izgleda ispravno sve dok producent ne upiše veličinu fonta u matricu. Drugi je popravak ispravnosti koji nas je umalo koštao paralelnog ubrzanja renderiranja, i način na koji smo brzinu vratili vrijedan je poznavanja ako pišete bilo koji PDF uređaj na GDI-ju

Zašto se tekst sabije u nakupinu kad PDF koristi Tf 1?

Zato je stari advance kod dodavao text-space udaljenost izravno na translacijsku komponentu Tm, kao da text space i user space uvijek imaju istu skalu. Dosta producenata iz stvarnog svijeta postavi veličinu fonta na 1 s Tf i pravu veličinu nosi u text matrici. S /F1 1 Tf i 12 0 0 12 72 700 Tm, glif širine 500 jedinica pomakne se 0.5 u text spaceu, što je 6 točaka na stranici jednom kad ga Tm skalira. Stari renderer izvršio je Tm.e := Tm.e + Adv i pomaknuo pero 0.5 točaka. Svaki je glif sletio dvanaestina znaka iza prethodnoga, pa se redak tjelesnog teksta renderirao kao tamna mrlja uz lijevi rub dok je ista datoteka izgledala savršeno u svakom drugom pregledniku

Zašto se tekst sabije pod Tf 1 u HotPDF rendereru: s 12 0 0 12 72 700 Tm glif od 500 jedinica mora se pomaknuti 0.5 text-space jedinica, što Tm skalira na 6 točaka, dok je stari kod dodavao 0.5 izravno na Tm.e i renderirao redak tjelesnog teksta kao mrlju po dvanaestinu znaka po glifu
Producenti koji veličinu fonta kodiraju u text matrici tjerali su svaki glif da sleti dvanaestina znaka iza prethodnoga, mana nevidljiva na izlazu same biblioteke
// Content stream od producenta koji veličinu kodira u Tm, ne Tf:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// Stari advance (pojednostavljeno): udaljenost dodana na Tm.e kao da je user space
Adv := W * FontSize / 1000;                  // 0.5 za glif od 500 jedinica
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // Th samo na širini
Adv := Adv + CharSpace;                      // Tc bez skaliranja s Th
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // Tw pogrešno skaliran s Tfs
Tm.e := Tm.e + Adv;                          // ignorira Tm.a, Tm.b, Tm.c, Tm.d

// Stara TJ prilagodba: bez Th, i opet samo Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;

Tm.e prečac nije bila jedina mana u tom bloku. Word spacing Tw izražava se u neskaliranim text space jedinicama, a stari ga je kod množio s FontSize / 1000, pa je pod Tf 12 poravnati redak izgubio gotovo sav međuriječni razmak. Horizontalno skaliranje Th primjenjivalo se na širinu glifa, ali ne na Tc ni Tw, a TJ kerning prilagodba preskakala ga je posve. Nepaintajuća putanja koja pomiče nevidljivi text render moda 3, onakvog kakav koriste OCR tekstualni slojevi, i tekst unutar skrivenog optional contenta nosila je privatnu kopiju iste aritmetike, pa je sve što je crtano nakon nevidljivog runa startalo s krive pozicije. Bugovi text stanja u rendereru rijetko padaju glasno: poput bugova indeksa operanada i imena resursa koji su jednom nulirali Tc, Tw i Tz bez ijedne greške, i ovi su proizveli uvjerljive stranice na izlazu same biblioteke i padali samo na datotekama drugih producenata

Kako ISO 32000-1 §9.4.4 definira glyph advance?

ISO 32000-1 §9.4.4 definira advance posve u text spaceu i primjenjuje ga na text matricu kao translacijsku matricu, pa je odgovor izračunati tx prvo i pustiti Tm da radi skaliranje, rotaciju i skew. Za horizontalno pisanje, tx je ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, gdje je w0 širina glifa u tisućinkama em-a, Tj je TJ prilagodba, a Th je Tz podijeljen sa 100. Novi Tm jest [1 0 0 1 tx 0] × Tm, što je u HotPDF-u helper HPDFTranslateTextMatrix: X i Y dodaje kroz koeficijente matrice a, b, c i d umjesto da piše u e i f izravno. Po §9.3.3, Tw vrijedi samo za jednobajtni character kod 32, pa višebajtni CID kodovi nikad ne pokupe word spacing na horizontalnoj putanji. Isti helper sada vodi Td, TD, T*, operatore ' i ", TJ prilagodbe i putanju skrivenog teksta, što znači da jedna funkcija posjeduje pravilo

Glyph advance iz ISO 32000-1 9.4.4 u HotPDF rendereru: tx računat u text spaceu iz w0, Tj, Tfs, Tc, Tw i Th, pa primijenjen kroz HPDFTranslateTextMatrix tako da pomak prolazi preko koeficijenata matrice a, b, c i d, a Td, TD, TJ i putanja skrivenog teksta dijele jedno pravilo
Dodavanje advancea izravno na Tm.e radi samo kad je text space jednak user spaceu; usmjeravanje kroz koeficijente matrice drži skalirani, rotirani i nakošeni tekst ispravnim
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;

// Horizontalni glyph advance, 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 u text spaceu, neskaliran
Adv := Adv * State.Text.HorizScale / 100;    // Th vrijedi za cijeli zbroj
HPDFTranslateTextMatrix(Tm, Adv, 0);

// TJ brojčani element: isti prostor, isti Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);

Pozicioniranje glifova moralo je slijediti istu logiku. Kad ugrađena kontura nije dostupna i renderer pada natrag na GDI TextOutW, sada gradi punu matricu glifa iz CTM × Tm × rise × em scale, uključujući Th, i ugrađuje je s SetWorldTransform u GM_ADVANCED modu unutar para SaveDC / RestoreDC. GDI font stvara se na fiksnoj visini od 1000 jedinica i transform radi dimenzioniranje, pa rotirani i nakošeni tekst zadržava orijentaciju umjesto da se crta uspravno na transformiranoj ishodišnoj točki. Vertikalni writing mod jedina je namjerna asimetrija: font WMode 1 pomakne se niz y osu svojom vertikalnom metrikom, i horizontalno skaliranje ne vrijedi na toj osi

Što q/Q zapravo sprema u PDF graphics state?

ISO 32000-1 §8.4.2 nabraja trenutačnu clipping putanju kao dio graphics statea, pa Q mora obnoviti clip točno onakvim kakav je bio na odgovarajućem q, a ne samo numeričke parametre. HotPDF je već držao stack graphics statea s CTM, bojama, parametrima linije i text stanjem, ali GDI drži clip u device contextu, izvan tog stacka. Kopija numeričkog stanja zato je obnavljala sve osim clipa, i clip ugrađen s W n unutar bloka q ... Q nastavljao je kosit svaku kasniju operaciju na stranici. Form XObjecti dodali su drugi put do istog kvara, jer §8.10 daje formi implicitno spremanje i oporavak oko svog sadržaja, i sadržaj formi iz stvarnog svijeta ponekad ostavi vlastite q operatore neuparene iako specifikacija traži da se upare. Renderer sada zove CaptureClipBeforeChange i SaveDC prije pokretanja forme, pa nakon što forma završi odbaci svake spremljene regije dublje od ulazne dubine i zove RestoreDC, pa svaka spremljena HRGN ima točno jedan put puštanja

Lijeno hvatanje clipa s THPDFSavedClipState

Popravak koji je otplovio sprema jedan zapis THPDFSavedClipState po q, ali skupi dio odgađa dok okvir prvi put ne promijeni clip. Zapis drži handle regije, dubinu stacka kojoj pripada, device context iz kojeg je uzeta i flag Captured. DevPushState samo upiše dubinu i DC i rasteže polje okvira udvostručavanjem od 16, pa content stream pun q 1 0 0 1 x y cm ... Q ne alocira nijedan GDI objekt. Operatori koji su pred promjenom clippinga — path slikanje s obnog W ili W*, operator n, pattern ispune i ulaz u formu — prvo zovu CaptureClipBeforeChange

Lijeno hvatanje GDI clipa u HotPDF rendereru: DevPushState bilježi samo dubinu i DC po q, CaptureClipBeforeChange čita regiju tik prije nego W, n ili ulaz u formu promijene clipping, DevPopState obnavlja i briše je na Q, a gorljiva verzija koja je hvatala na svakom q spustila je paralelni throughput na oko 1.15 puta jednonavojnog
Stvaranje GDI regije na svakom q izgladnilo je render navoje, pa se hvatanje sada događa samo kad je operator pred promjenom clippinga i gate ubrzanja 1.5x ponovno prolazi
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;   // već spremljeno, ili nije naše
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // 0 znači da clipa uopće nema
  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 skida clip
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

Izmjereni trošak gorljive verzije razlog je postojanja ovog dizajna. Prva ispravna implementacija stvarala je i čitala GDI regiju na svakom q, i na stranicama sačinjenim uglavnom od numeričkih transforma renderer su navoji provodili vrijeme nadmudrujući se za GDI region objekte umjesto rasterizirajući. Paralelni render cjevovod pao je s očekivanog dobitka na otprilike 1.13 do 1.20 puta jednonavojnog throughputa i pao je gate ubrzanja 1.5x u benchmark suiteu. S lijenim hvatanjem i ponovno korištenim kapacitetom okvira isti benchmark ponovno prolazi izvorni gate 1.5x. Sitni TrueType antialiasing glifova sletio je u isto izdanje i bio je očit sumnjivac, ali se regresija pratila do alokacije regija, što je dobra opomena da se mjeri prije nego se okrivi najnovija značajka

Gdje su granice ovog pristupa?

Spremljeni clip GDI je regija u device pikselima, pa je točna za bitmapu koja se renderira i besmislena za bilo koji drugi cilj. Zato svaki okvir bilježi svoj device context i DevPopState preskače oporavak kad se DC promijenio, na primjer dok transparency grupa renderira u vlastitu layer bitmapu. GetClipRgn koja vrati nulu valjan je rezultat koji znači da clipa nema, i njezino obnavljanje s SelectClipRgn(FDC, 0) točno skida clip koji nije postojao na odgovarajućem q. Na strani teksta popravak ispravlja kamo svaki glif ide, ali ne izmišlja širine: ako font izostavi svoje /Widths polje, a ugrađeni program nije dostupan, advance je i dalje dobar onoliko koliko dobar je width fallback. Kad ovo područje regresijski testirate, držite barem jednu fiksuru s Tf 1 i skaliranom Tm, jednu s ne-nultim Tz i Tw, i jednu s clipom unutar q ... Q iza kojeg slijedi sadržaj izvan njega, jer se nijedno od toga ne pojavljuje u dokumentima koje generira sama biblioteka

Ako renderer pogonite iz aplikacijskog koda, u obrascu pozivanja opisanom u članku o renderiranju PDF stranice u bitmapu ništa se ne mijenja, i stranice koje su prije pokazivale zamazane retke ili izrezan sadržaj trebaju se jednostavno ispravno renderirati na 2.754.0 i kasnijim. Detalji o komponenti, podržanim verzijama Delphija i C++Buildera i licenciranju su na stranici proizvoda HotPDF Delphi PDF Component