De paginarenderer van HotPDF Delphi Component schuift tekst nu op door elke glyph-verplaatsing in tekstruimte te berekenen, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th zoals ISO 32000-1 §9.4.4 die definieert, en daarna de tekstmatrix via zijn lineaire deel te verplaatsen met HPDFTranslateTextMatrix. Clippen wordt per q-frame bewaard en bij Q hersteld, maar een GDI-regio wordt alleen vastgelegd wanneer dat frame de clip werkelijk verandert. Allebei de fixes zitten in HotPDF 2.754.0, en allebei komen uit echte pagina's die rendeerden met ineengedrukte woorden of clipregio's die voorbij hun Q lekten. De eerste bug is rekenwerk dat er correct uitziet tot een producent zijn fontgrootte in de matrix stopt. De tweede is een correctheidsfix die ons bijna de parallelle rendersnelheid kostte, en hoe we de snelheid terugkregen is de moeite van het weten waard als u zelf een GDI-gebaseerd PDF-apparaat schrijft
Waarom klontert tekst samen wanneer een PDF Tf 1 gebruikt?
Omdat de oude advance-code een tekstruimte-afstand rechtstreeks bij de translatiecomponent van Tm optelde, alsof tekstruimte en gebruikersruimte altijd dezelfde schaal hadden. Legio echte producenten zetten de fontgrootte op 1 met Tf en dragen de echte grootte in de tekstmatrix. Met /F1 1 Tf en 12 0 0 12 72 700 Tm schuift een glyph van 500 units breed 0.5 op in tekstruimte, wat 6 punten op de pagina is zodra Tm hem schaalt. De oude renderer voerde Tm.e := Tm.e + Adv uit en verplaatste de pen 0.5 punt. Elke glyph landde één twaalfde teken na de vorige, dus een regel lopende tekst rendereerde als een donkere vlek aan de linkermarge terwijl hetzelfde bestand er in elke andere viewer perfect uitzag
// Content stream van een producent die de grootte in Tm codeert, niet in Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Oude advance (vereenvoudigd): afstand rechtstreeks op Tm.e alsof het user space is
Adv := W * FontSize / 1000; // 0.5 voor een glyph van 500 units
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th alleen op de breedte
Adv := Adv + CharSpace; // Tc niet geschaald door Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw ten onrechte geschaald door Tfs
Tm.e := Tm.e + Adv; // negeert Tm.a, Tm.b, Tm.c, Tm.d
// Oude TJ-aanpassing: geen Th, en weer alleen Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
De Tm.e-shortcut was niet het enige defect in dat blok. Woordafstand Tw wordt uitgedrukt in ongeschaalde tekstruimte-units, toch vermenigvuldigde de oude code haar met FontSize / 1000, dus onder Tf 12 verloor een uitgevulde regel bijna zijn hele woordafstand. Horizontale schaling Th gold voor de glyphbreedte maar niet voor Tc of Tw, en de TJ-kerning-aanpassing sloeg haar helemaal over. Het niet-tekenende pad dat render mode 3 onzichtbare tekst voortbeweegt, het soort dat OCR-tekstlagen gebruiken, en tekst binnen verborgen optional content droegen een privé-kopie van dezelfde rekenkunde, dus alles wat na een onzichtbare run werd getekend begon op de verkeerde positie. Tekststatus-bugs in een renderer falen zelden luid: net als de operand-index- en resourcenaam-bugs die ooit Tc, Tw en Tz op nul zetten zonder één foutmelding, leverden deze geloofwaardige pagina's op de eigen uitvoer van de library en braken ze alleen op bestanden van andere producenten
Hoe definieert ISO 32000-1 §9.4.4 de glyph-advance?
ISO 32000-1 §9.4.4 definieert de advance volledig in tekstruimte en past haar toe op de tekstmatrix als translatiematrix, dus het antwoord is eerst tx te berekenen en Tm de schaling, rotatie en skew te laten doen. Voor horizontaal schrijven geldt tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, waarbij w0 de glyphbreedte in duizendsten van een em is, Tj de TJ-aanpassing, en Th Tz gedeeld door 100. De nieuwe Tm is [1 0 0 1 tx 0] × Tm, wat in HotPDF de helper HPDFTranslateTextMatrix is: hij telt X en Y op via de matrixcoëfficiënten a, b, c en d in plaats van rechtstreeks naar e en f te schrijven. Volgens §9.3.3 geldt Tw alleen voor de single-byte-characternummer 32, dus multibyte-CID-nummers pikken op het horizontale pad nooit woordafstand op. Dezelfde helper drijft nu Td, TD, T*, de '- en "-operators, TJ-aanpassingen en het verborgen-tekst-pad, wat betekent dat één functie de regel bezit
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;
// Horizontale 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 in tekstruimte, ongeschaald
Adv := Adv * State.Text.HorizScale / 100; // Th geldt voor de hele som
HPDFTranslateTextMatrix(Tm, Adv, 0);
// TJ-nummerelement: dezelfde ruimte, dezelfde Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Glyphplaatsing moest dezelfde logica volgen. Wanneer er geen ingebedde outline beschikbaar is en de renderer terugvalt op GDI-TextOutW, bouwt hij nu de volledige glyphmatrix uit CTM × Tm × rise × em-schaal, inclusief Th, en installeert haar met SetWorldTransform in GM_ADVANCED-modus binnen een SaveDC-/ RestoreDC-paar. Het GDI-font wordt aangemaakt op een vaste hoogte van 1000 units en de transformatie doet het schalen, dus gedraaide en geschuinde tekst houdt zijn oriëntatie in plaats van rechtop getekend te worden op een getransformeerd oorsprongspunt. Verticale schrijfrichting is de enige bewuste asymmetrie: een WMode 1-font schuift over de y-as omlaag met zijn verticale metriek, en horizontale schaling geldt niet voor die as
Wat bewaart q/Q eigenlijk in een PDF-grafiektoestand?
ISO 32000-1 §8.4.2 somt het huidige clipping path op als deel van de graphics state, dus Q moet de clip exact herstellen zoals die was bij de matchende q, en niet alleen de numerieke parameters. HotPDF hield al een graphics-state-stack bij met de CTM, kleuren, lijnparameters en tekststatus, maar GDI bewaart de clip in de device context, buiten die stack. Een kopie van de numerieke toestand herstelde dus alles behalve de clip, en een clip geïnstalleerd met W n binnen een q ... Q-blok bleef elke latere operatie op de pagina bijsnijden. Form XObjects voegden een tweede route naar hetzelfde falen toe, want §8.10 geeft een form een impliciete save en restore rond zijn content, en echte formuliercontent laat zijn eigen q-operators soms ongepaard achter zelfs al eist de specificatie dat ze paren. De renderer roept nu CaptureClipBeforeChange en SaveDC aan vóór het draaien van een form, en gooit na het afronden van de form elke bewaarde regio dieper dan de instapdiepte weg en roept RestoreDC aan, zodat elke bewaarde HRGN precies één vrijgavepad heeft
Lazy clip-vastlegging met THPDFSavedClipState
De fix die is uitgekomen bewaart één THPDFSavedClipState-record per q, maar stelt het dure deel uit tot het frame de clip voor het eerst wijzigt. Het record houdt de region-handle vast, de stackdiepte waar hij bij hoort, de device context waar hij vandaan kwam en een Captured-flag. DevPushState vult alleen de diepte en de DC in en groeit de frame-array door te verdubbelen vanaf 16, dus een content stream vol q 1 0 0 1 x y cm ... Q alloceert helemaal geen GDI-object. Operators die op het punt staan clipping te veranderen — dus padtekening met een hangende W of W*, de n-operator, pattern fills en formulierinstap — roepen eerst CaptureClipBeforeChange aan
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; // al bewaard, of niet die van ons
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 betekent helemaal geen 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 verwijdert de clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
De gemeten kosten van de eager-versie zijn de reden dat dit ontwerp bestaat. De eerste correcte implementatie maakte bij elke q een GDI-regio aan en las haar uit, en op pagina's die vooral uit numerieke transformaties bestonden verkorsten de rendererthreads hun tijd aan het beconcurreren om GDI-regio-objecten in plaats van te rasteriseren. De parallelle renderpipeline zakte van de verwachte winst naar grofweg 1.13 tot 1.20 keer single-threaded doorvoer en zakte voor de snelheidsgate van 1.5 keer in de benchmarksuite. Met lazy vastlegging en hergebruikte framecapaciteit haalt dezelfde benchmark de oorspronkelijke gate van 1.5 keer weer. Kleine TrueType-glyph-antialiasing landde in dezelfde release en was de voor de hand liggende verdachte, maar de regressie traceerde terug naar regio-allocatie, een goede herinnering om te meten voordat u de nieuwste feature de schuld geeft
Waar liggen de grenzen van deze aanpak?
De bewaarde clip is een GDI-regio in device-pixels, dus ze is exact voor de bitmap die wordt gerenderd en betekenisloos voor elk ander doel. Daarom legt elk frame zijn device context vast en slaat DevPopState het herstel over wanneer de DC is veranderd, bijvoorbeeld terwijl een transparency group in zijn eigen layer-bitmap rendert. GetClipRgn die nul teruggeeft is een legitiem resultaat dat geen clip betekent, en haar herstellen met SelectClipRgn(FDC, 0) is precies wat een clip correct verwijdert die bij de matchende q niet bestond. Aan de tekstkant corrigeert de fix waar elke glyph terechtkomt, maar ze verzint geen breedtes: laat een font zijn /Widths-array weg en is het ingebedde programma onbeschikbaar, dan is de advance nog steeds zo goed als de width-fallback. Regression-test u dit gebied, houd dan minstens één fixture met Tf 1 en een geschaalde Tm, één met een niet-nul Tz en Tw, en één met een clip binnen q ... Q gevolgd door content erbuiten, want geen van alle komt voor in documenten die de library zelf genereert
Stuurt u de renderer vanuit applicatiecode aan, dan verandert er niets aan het aanroeppatroon uit een PDF-pagina naar een bitmap renderen, en pagina's die eerder vervaagde regels of afgeknipte content toonden renderen op 2.754.0 en nieuwer simpelweg correct. Details over de component, de ondersteunde Delphi- en C++Builder-versies en licentiëring staan op de productpagina van HotPDF Delphi PDF Component