Sidrenderaren i HotPDF Delphi Component flyttar nu text framåt genom att beräkna varje glyfdisplacement i text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th som ISO 32000-1 §9.4.4 definierar det, och flyttar sedan textmatrisen genom dess linjära del med HPDFTranslateTextMatrix. Clippning sparas per q-ram och återställs på Q, men en GDI-region fångas bara när den ramen faktiskt ändrar clippen. Båda fixarna kom i HotPDF 2.754.0, och båda kom från verkliga sidor som renderades med ihopkollapsade ord eller clipregioner som läckte förbi sin Q. Den första buggen är aritmetik som ser rätt ut tills en producent skriver sin fontstorlek i matrisen. Den andra är en korrekthetsfix som nästan kostade oss parallelrenderingshastigheten, och sättet vi fick tillbaka hastigheten på är värt att känna till om du skriver någon GDI-baserad PDF-enhet
Varför kollapsar text till en klump när en PDF använder Tf 1?
För att den gamla advance-koden lade till ett text space-avstånd rakt på translationkomponenten i Tm, som om text space och user space alltid haft samma skala. Massor av verkliga producenter sätter fontstorleken till 1 med Tf och bär den riktiga storleken i textmatrisen. Med /F1 1 Tf och 12 0 0 12 72 700 Tm flyttar sig en glyf 500 enheter bred 0,5 i text space, vilket är 6 punkter på sidan när Tm skalar den. Den gamla renderaren exekverade Tm.e := Tm.e + Adv och flyttade pennan 0,5 punkter. Varje glyf landade en tolvtedel av ett tecken efter den föregående, så en rad med brödtext renderades som en mörk smet vid vänstermarginalen medan samma fil såg perfekt ut i varje annan viewer
// Innehållsström från en producent som kodar storleken i Tm, inte Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Gammal advance (förenklad): avstånd tillagt på Tm.e som vore det user space
Adv := W * FontSize / 1000; // 0.5 för en glyf på 500 enheter
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th endast på bredden
Adv := Adv + CharSpace; // Tc skalas inte med Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw felaktigt skalad med Tfs
Tm.e := Tm.e + Adv; // ignorerar Tm.a, Tm.b, Tm.c, Tm.d
// Gammal TJ-justering: ingen Th, och åter bara Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
Tm.e-genvägen var inte den enda defekten i det blocket. Ordavstånd Tw uttrycks i oskalade text space-enheter, ändå multiplicerade den gamla koden det med FontSize / 1000, så under Tf 12 förlorade en justerad rad nästan hela sitt mellanordsmellanrum. Horisontalskalning Th gällde glyfbredden men inte Tc eller Tw, och TJ-kerningjusteringen hoppade över den helt. Den icke-målande vägen som flyttar fram render mode 3 osynlig text, den sort OCR-textlager använder, och text inuti dolt optional content bar en privat kopia av samma aritmetik, så allting ritat efter ett osynligt lopp startade från fel position. Texttillståndsbuggar i en renderare fallerar sällan högljutt: som operandindex- och resursnamnsbuggarna som en gång nollställde Tc, Tw och Tz utan ett enda fel, gav dessa trovärdiga sidor på bibliotekets egen utdata och gick bara sönder på filer från andra producenter
Hur definierar ISO 32000-1 §9.4.4 glyfavståndet?
ISO 32000-1 §9.4.4 definierar avståndet helt i text space och tillämpar det på textmatrisen som en translationsmatris, så svaret är att beräkna tx först och låta Tm sköta skalning, rotation och skevning. För horisontell skrift är tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, där w0 är glyfbredden i tusendelar av en em, Tj är TJ-justeringen, och Th är Tz delat med 100. Den nya Tm är [1 0 0 1 tx 0] × Tm, vilket i HotPDF är hjälparen HPDFTranslateTextMatrix: den adderar X och Y genom matriskoefficienterna a, b, c och d i stället för att skriva till e och f direkt. Enligt §9.3.3 gäller Tw bara för enbytesteckenkoden 32, så flerbajts-CID-koder plockar aldrig upp ordmellanrum på den horisontella vägen. Samma hjälpare driver nu Td, TD, T*, operatorerna ' och ", TJ-justeringar och den dolda textvägen, vilket betyder att en enda funktion äger regeln
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;
// Horisontellt glyfavstånd, 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 i text space, oskalad
Adv := Adv * State.Text.HorizScale / 100; // Th gäller hela summan
HPDFTranslateTextMatrix(Tm, Adv, 0);
// TJ-nummerelement: samma space, samma Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Glyfplaceringen fick följa samma logik. När ingen inbäddad kontur är tillgänglig och renderaren faller tillbaka på GDI TextOutW bygger den nu hela glyfmatrisen ur CTM × Tm × rise × em-skala, inklusive Th, och installerar den med SetWorldTransform i GM_ADVANCED-läge inuti ett SaveDC/RestoreDC-par. GDI-fonten skapas med en fast höjd på 1000 enheter och transformen gör storlekssättningen, så roterad och skev text behåller sin orientering i stället för att ritas upprätt vid en transformerad ursprungspunkt. Vertikalt skrivläge är den enda medvetna asymmetrin: en WMode 1-font flyttar sig nedåt längs y-axeln med sitt vertikala mått, och horisontalskalning gäller inte den axeln
Vad sparar q/Q egentligen i ett PDF-grafikläge?
ISO 32000-1 §8.4.2 listar den aktuella clippningsvägen som del av grafikläget, så Q måste återställa clippen exakt som den var vid matchande q, inte bara de numeriska parametrarna. HotPDF höll redan en grafiktillståndsstack med CTM, färger, linjeparametrar och textläge, men GDI håller clippen i device context, utanför den stacken. En kopia av det numeriska tillståndet återställde därför allting utom clippen, och en clip installerad med W n inuti ett q ... Q-block fortsatte att beskärja varje senare operation på sidan. Form XObjects lade till en andra väg till samma fel, eftersom §8.10 ger en form en implicit save och restore runt sitt innehåll, och verkligt forminnehåll lämnar ibland sina egna q-operatorer obalanserade även om specifikationen kräver att de parar ihop. Renderaren anropar nu CaptureClipBeforeChange och SaveDC innan den kör en form, och efter att formen är klar kastar den sparade regioner djupare än ingångsdjupet och anropar RestoreDC, så varje sparad HRGN har exakt en release-väg
Lazy clip-fångst med THPDFSavedClipState
Fixen som levererades sparar en THPDFSavedClipState-post per q, men skjuter upp den dyra delen tills ramen först ändrar clippen. Posten håller regionhandtaget, det stackdjup den hör till, den device context den togs ifrån och en Captured-flagga. DevPushState fyller bara i djupet och DC:n och växer frame-arrayen genom att fördubbla från 16, så en innehållsström full av q 1 0 0 1 x y cm ... Q allokerar inget GDI-objekt alls. Operatorer som håller på att ändra clippning, alltså sökvägsmålning med ett väntande W eller W*, operatorn n, mönsterfyllningar och formingång, anropar CaptureClipBeforeChange först
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; // redan sparad, eller inte vår
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 betyder ingen clip alls
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 tar bort clippen
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
Den uppmätta kostnaden för den ivriga versionen är skälet till att den här designen finns. Den första korrekta implementationen skapade och läste en GDI-region vid varje q, och på sidor mest uppbyggda av numeriska transformeringar spenderade rendertrådarna sin tid på att konkurrera om GDI-regionobjekt i stället för att rasterisera. Den parallella renderpipelinen föll från sin förväntade vinst till ungefär 1,13 till 1,20 gånger enkeltrådad genomströmning och misslyckades med grinden för 1,5 gånger snabbare i benchmarksviten. Med lazy-fångst och återanvänd frame-kapacitet passerar samma benchmark den ursprungliga grinden på 1,5 gånger igen. Liten TrueType-glyfantialiasing landade i samma release och var den uppenbara misstänkta, men regressionen spårades tillbaka till regionallokering, vilket är en bra påminnelse om att mäta innan man skyller på den nyaste funktionen
Var ligger gränserna för det här upplägget?
Den sparade clippen är en GDI-region i enhetspixlar, så den är exakt för bitmappen som renderas och meningslös för varje annat mål. Det är därför varje ram registrerar sin device context och DevPopState hoppar över återställningen när DC:n har ändrats, till exempel medan en transparengrupp renderar in i sin egen lagerbitmap. GetClipRgn som returnerar noll är ett legitimt resultat som betyder ingen clip, och att återställa den med SelectClipRgn(FDC, 0) är vad som korrekt tar bort en clip som inte fanns vid matchande q. På textsidan korrigerar fixen vart varje glyf går, men den hittar inte på bredder: om en font utelämnar sin /Widths-array och det inbäddade programmet är otillgängligt är avståndet fortfarande bara så bra som breddfallbacken. När du regressionstestar det här området, behåll minst en fixtur med Tf 1 och en skalad Tm, en med icke-noll Tz och Tw, och en med en clip inuti q ... Q följt av innehåll utanför den, för att inget av dem dyker upp i dokument genererade av biblioteket självt
Om du driver renderaren från applikationskod ändras ingenting i anropsmönstret som beskrivs i att rendera en PDF-sida till en bitmap, och sidor som tidigare visade utsmetade rader eller beskuret innehåll ska helt enkelt renderas korrekt på 2.754.0 och senare. Detaljer om komponenten, stödda Delphi- och C++Builder-versioner och licensiering finns på produktsidan för HotPDF Delphi PDF Component