Page renderer HotPDF Delphi Component teď posouvá text počítáním každého posunu glyfu v text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, jak ho definuje ISO 32000-1 §9.4.4, a pak pohybuje text maticí přes její lineární část s HPDFTranslateTextMatrix. Clipping se ukládá na každý rámec q a obnovuje na Q, ale GDI region se zachytí jen když ten rámec clip skutečně mění. Obě opravy přistály v HotPDF 2.754.0 a obě přišly z reálných stránek, které se renderovaly se sbalenými slovy nebo s clip regiony přetékajícími za jejich Q. První bug je aritmetika, která vypadá správně, dokud producent nezapíše svou velikost fontu do matice. Druhá je oprava správnosti, která nás málem stála paralelní zrychlení renderu, a způsob, jak jsme rychlost dostali zpátky, stojí za znalost, pokud píšete jakékoli GDI-backed PDF zařízení
Proč se text sbalí do chuchvalce, když PDF používá Tf 1?
Protože starý advance kód přičítal vzdálenost v text space rovnou k translační složce Tm, jako by text space a user space měly vždy stejnou škálu. Spousta reálných producentů nastaví velikost fontu na 1 přes Tf a skutečnou velikost veze v text matici. S /F1 1 Tf a 12 0 0 12 72 700 Tm posune se glyf široký 500 jednotek o 0.5 v text space, což je na stránce 6 bodů, jakmile to Tm škáluje. Starý renderer provedl Tm.e := Tm.e + Adv a pohnul perem o 0.5 bodu. Každý glyf přistál jedna dvanáctina znaku po předchozím, takže řádek textu se vyrenderoval jako tmavý smyk u levého okraje, zatímco tentýž soubor vypadal v každém jiném prohlížeči dokonale
// Content stream od producenta, který kóduje velikost v Tm, ne Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Starý advance (zjednodušeno): vzdálenost přičtená k Tm.e, jako by to byl user space
Adv := W * FontSize / 1000; // 0.5 pro glyf o 500 jednotkách
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th jen na šířce
Adv := Adv + CharSpace; // Tc neškálované Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw chybně škálované Tfs
Tm.e := Tm.e + Adv; // ignoruje Tm.a, Tm.b, Tm.c, Tm.d
// Stará TJ úprava: žádné Th a zase jen Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
Zkratka Tm.e nebyla jediná vada v tom bloku. Word spacing Tw se vyjadřuje v neškálovaných jednotkách text space, a přesto ho starý kód násobil FontSize / 1000, takže pod Tf 12 ztratil zarovnaný řádek skoro celou svou mezeru mezi slovy. Horizontální škálování Th se aplikovalo na šířku glyfu, ale ne na Tc nebo Tw a TJ kerning úprava ho vynechala úplně. Nepaintovací cesta posouvající neviditelný text render mode 3, jaký používají OCR textové vrstvy, a text uvnitř skrytého optional contentu nesla soukromou kopii týže aritmetiky, takže cokoli nakreslené po neviditelném běhu startovalo ze špatné pozice. Chyby text stavu v rendereru málokdy selžou hlasitě: jako bugy operand indexu a resource name, které jednou vynulovaly Tc, Tw i Tz bez jediné chyby, produkovaly věrohodné stránky na vlastním výstupu knihovny a rozbily se jen na souborech od jiných producentů
Jak definuje ISO 32000-1 §9.4.4 glyph advance?
ISO 32000-1 §9.4.4 definuje advance úplně v text space a aplikuje ho na text matici jako translační matici, takže odpověď je spočítat nejdřív tx a nechat Tm dělat škálování, rotaci i skew. Pro horizontální psaní se tx rovná ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, kde w0 je šířka glyfu v tisícinách em, Tj je TJ úprava a Th je Tz dělené 100. Nové Tm je [1 0 0 1 tx 0] × Tm, což je v HotPDF helper HPDFTranslateTextMatrix: přičítá X a Y přes maticové koeficienty a, b, c a d místo zápisu přímo do e a f. Podle §9.3.3 se Tw aplikuje jen na jednobajtový character code 32, takže multibytové CID kódy nikdy nesbírají word spacing na horizontální cestě. Týž helper teď řídí Td, TD, T*, operátory ' a ", TJ úpravy i skrytou textovou cestu, což znamená, že jediná funkce 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ální 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 v text space, neškálované
Adv := Adv * State.Text.HorizScale / 100; // Th se aplikuje na celý součet
HPDFTranslateTextMatrix(Tm, Adv, 0);
// TJ number element: týž prostor, týž Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Umístění glyfů muselo následovat tutéž logiku. Když není k dispozici žádný vložený obrys a renderer spadne zpátky na GDI TextOutW, staví teď plnou glyfovou matici z CTM × Tm × rise × em škála, včetně Th, a instaluje ji přes SetWorldTransform v módu GM_ADVANCED uvnitř páru SaveDC / RestoreDC. GDI font se vytváří s fixní výškou 1000 jednotek a transformu dělá sizing, takže rotovaný i zkosený text si drží orientaci místo kreslení vzpřímeně na transformovaném bodu původu. Vertikální writing mode je jedna záměrná asymetrie: font WMode 1 posouvá dolů po ose y o svou vertikální metriku a horizontální škálování se na tu osu nevztahuje
Co q/Q doopravdy ukládá v PDF graphics state?
ISO 32000-1 §8.4.2 řadí aktuální clipping path mezi graphics state, takže Q musí obnovit clip přesně tak, jak byl při odpovídajícím q, ne jen číselné parametry. HotPDF už vedl zásobník graphics state s CTM, barvami, parametry čar a text stavem, ale GDI drží clip v device contextu, mimo ten zásobník. Kopie číselného stavu tedy obnovila všechno kromě clipu a clip instalovaný W n uvnitř bloku q ... Q pořád ořezával každou pozdější operaci na stránce. Form XObjects přidaly druhou cestu ke stejnému selhání, protože §8.10 dává formě implicitní save a restore kolem svého obsahu a reálný obsah formulářů někdy nechává vlastní operátory q nevyvážené, i když specifikace vyžaduje, aby se párovaly. Renderer teď volá CaptureClipBeforeChange a SaveDC před spuštěním formy a po dokončení formy zahodí jakékoli uložené regiony hlubší než vstupní hloubka a zavolá RestoreDC, takže každý uložený HRGN má přesně jednu uvolňovací cestu
Líné zachytávání clipu s THPDFSavedClipState
Oprava, která vyšla, ukládá jeden záznam THPDFSavedClipState na q, ale odkládá drahou část do doby, než rámec poprvé změní clip. Záznam drží handle regionu, hloubku zásobníku, do níž patří, device context, ze kterého byl vzat, a příznak Captured. DevPushState vyplní jen hloubku a DC a roste polem rámců zdvojnásobováním od 16, takže content stream plný q 1 0 0 1 x y cm ... Q nealokuje žádný GDI objekt. Operátory, které se chystají měnit clipping, tedy path painting s čekajícím W nebo W*, operátor n, pattern fills a vstup do formy, volají nejdřív 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; // already saved, or not ours
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 znamená žádný 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 odstraní clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
Naměřená cena horlivé verze je důvod existence tohohle designu. První správná implementace vytvářela a četla GDI region na každém q a na stránkách složených hlavně z číselných transformací trávily render vlákna čas přetížením GDI region objektů místo rasterizace. Paralelní render pipeline spadla z očekávaného zisku na zhruba 1.13 až 1.20krát single-threaded throughput a neprošla branou 1.5násobného zrychlení v benchmark sadě. S líným zachytáváním a znovupoužívanou kapacitou rámců tatáž benchmark prochází původní branou 1.5násobného zrychlení znovu. Malé TrueType glyph antialiasing přistálo ve stejném release a bylo očividným podezřelým, ale regrese se dohledala k alokaci regionů, což je dobrá připomínka měřit, než obviníte nejnovější funkci
Kde jsou limity tohohle přístupu?
Uložený clip je GDI region v device pixelech, takže je přesný pro bitmapu, která se renderuje, a bezvýznamný pro jakýkoli jiný cíl. Proto každý rámec zaznamenává svůj device context a DevPopState přeskočí obnovu, když se DC změnil, například zatímco transparency group renderuje do vlastní layer bitmapy. GetClipRgn vracející nulu je legitimní výsledek znamenající žádný clip a jeho obnova přes SelectClipRgn(FDC, 0) je to, co správně odstraní clip, který při odpovídajícím q neexistoval. Na textové straně oprava spraví, kam každý glyf jde, ale nevynalézá šířky: pokud font vynechá své pole /Widths a vložený program není k dispozici, advance je pořád jen tak dobrý, jaký je width fallback. Když tahle oblast prochází regresním testováním, držte aspoň jednu fixturu s Tf 1 a škálovaným Tm, jednu s nenulovým Tz a Tw a jednu s clipem uvnitř q ... Q následovaným obsahem vně něj, protože žádné z nich se neukáže v dokumentech generovaných samotnou knihovnou
Pokud renderer řídíte z aplikačního kódu, volací vzorec popsaný v článku o renderování PDF stránky na bitmapu se nemění a stránky, které dřív ukazovaly rozmazané řádky nebo ořezaný obsah, by se na 2.754.0 a novějším měly prostě vyrenderovat správně. Detaily o komponentě, podporovaných verzích Delphi a C++Builderu a licencování jsou na stránce produktu HotPDF Delphi PDF Component