HotPDF Delphi Component side-rendereren advancerer nu tekst ved at beregne hver glyph-forskydning i text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, som ISO 32000-1 §9.4.4 definerer den, og flytter derefter text matrix gennem dens lineære del med HPDFTranslateTextMatrix. Clipping gemmes pr. q-frame og gendannes på Q, men en GDI-region captureres først, når den frame faktisk ændrer clippen. Begge fixes landede i HotPDF 2.754.0, og begge kom fra rigtige sider, der renderede med kollapset ord eller clip-regioner, der lækkede forbi deres Q. Den første bug er aritmetik, der ser rigtig ud, indtil en producent skriver sin fontstørrelse ind i matrixen. Den anden er en korrektheds-fix, der næsten kostede os parallel-render-speedup, og måden, vi fik farten tilbage på, er værd at kende, hvis du skriver en GDI-baseret PDF-enhed
Hvorfor kollapser tekst til en klump, når en PDF bruger Tf 1?
Fordi den gamle advance-kode lagde en text space-distance direkte til translationskomponenten af Tm, som om text space og user space altid havde samme skala. Masser af rigtige producenter sætter fontstørrelsen til 1 med Tf og bærer den rigtige størrelse i text matrix. Med /F1 1 Tf og 12 0 0 12 72 700 Tm advancerer en glyph på 500 enheder 0,5 i text space, hvilket er 6 punkter på siden, når Tm skalerer den. Den gamle renderer eksekverede Tm.e := Tm.e + Adv og flyttede pennen 0,5 punkter. Hver glyph landede en tolvtedel af et tegn efter den forrige, så en linje brødtekst renderede som en mørk smear ved venstre margen, mens samme fil så perfekt ud i alle andre viewers
// Content stream fra en producent, der encoder størrelse i Tm, ikke Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Gammel advance (forenklet): distance lagt til Tm.e, som om det var user space
Adv := W * FontSize / 1000; // 0.5 for en glyph på 500 enheder
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th kun på bredden
Adv := Adv + CharSpace; // Tc ikke skaleret med Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw forkert skaleret med Tfs
Tm.e := Tm.e + Adv; // ignorerer Tm.a, Tm.b, Tm.c, Tm.d
// Gammel TJ-justering: ingen Th, og igen kun Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
Tm.e-genvejen var ikke den eneste defekt i den blok. Word spacing Tw udtrykkes i uskalerte text space-enheder, men den gamle kode multiplicerede den med FontSize / 1000, så under Tf 12 mistede en justeret linje næsten hele sit mellem-ords-mellemrum. Horisontal skalering Th galdt glyph-bredden men ikke Tc eller Tw, og TJ-kerning-justeringen skippede den helt. Den ikke-malende sti, der advancerer render mode 3 usynlig tekst — den slags, OCR-tekstlag bruger — og tekst inde i skjult optional content bar en privat kopi af samme aritmetik, så alt, der blev tegnet efter et usynligt run, startede fra den forkerte position. Teksttilstands-bugs i en renderer fejler sjældent højt: ligesom operand-indeks- og resource name-buggene, der engang nulstillede Tc, Tw og Tz uden én eneste fejl, producerede de troværdige sider på bibliotekets eget output og brød først på filer fra andre producenter
Hvordan definerer ISO 32000-1 §9.4.4 glyph-advance?
ISO 32000-1 §9.4.4 definerer advance udelukkende i text space og anvender den på text matrix som en translationsmatrix, så svaret er at beregne tx først og lade Tm stå for skalering, rotation og skew. For vandret skrivning er tx lig ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, hvor w0 er glyph-bredden i tusindedele af en em, Tj er TJ-justeringen, og Th er Tz divideret med 100. Den nye Tm er [1 0 0 1 tx 0] × Tm, hvilket i HotPDF er helperen HPDFTranslateTextMatrix: den lægger X og Y til gennem matrixkoefficienterne a, b, c og d i stedet for at skrive direkte til e og f. Ifølge §9.3.3 gælder Tw kun for single-byte-tegncoden 32, så multibyte CID-koder får aldrig word spacing på den horisontale sti. Samme helper driver nu Td, TD, T*, operatorerne ' og ", TJ-justeringer og den skjulte tekststi, hvilket betyder, at én enkelt funktion ejer reglen
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;
// Horisontal 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 i text space, uskaleret
Adv := Adv * State.Text.HorizScale / 100; // Th gælder hele summen
HPDFTranslateTextMatrix(Tm, Adv, 0);
// TJ-talelement: samme space, samme Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
Glyph-placering skulle følge samme logik. Når ingen embedded outline er tilgængelig, og rendereren falder tilbage til GDI TextOutW, bygger den nu den fulde glyph-matrix af CTM × Tm × rise × em-skala, inklusive Th, og installerer den med SetWorldTransform i GM_ADVANCED-mode inde i et SaveDC-/ RestoreDC-par. GDI-fonten oprettes med en fast højde på 1000 enheder, og transformen står for størrelsen, så roteret og skew'et tekst bevarer sin orientering i stedet for at blive tegnet oprejst på et transformeret origin-punkt. Vertikal skrivemode er den eneste bevidste asymmetri: en WMode 1-font advancerer ned ad y-aksen med sin vertikale metrik, og horisontal skalering gælder ikke den akse
Hvad gemmer q/Q faktisk i en PDF graphics state?
ISO 32000-1 §8.4.2 lister den aktuelle clipping path som en del af graphics state, så Q skal gendanne clippen præcis, som den var ved det matchende q, ikke kun de numeriske parametre. HotPDF havde allerede en graphics state-stak med CTM, farver, line-parametre og text state, men GDI holder clippen i device context, uden for den stak. En kopi af den numeriske tilstand gendannede derfor alt undtagen clippen, og en clip installeret med W n inde i en q ... Q-blok fortsatte med at beskære hver senere operation på siden. Form XObjects tilføjede en anden rute til samme fejl, for §8.10 giver en form et implicit save og restore omkring sit indhold, og rigtigt form-indhold efterlader nogle gange sine egne q-operatorer ubalanceret, selvom specifikationen kræver, at de går op. Rendereren kalder nu CaptureClipBeforeChange og SaveDC, før den kører en form, og efter formen er færdig, kasserer den alle gemte regioner dybere end entry-dybden og kalder RestoreDC, så hver gemt HRGN har præcis én release-sti
Lazy clip-capture med THPDFSavedClipState
Fixet, der shippede, gemmer én THPDFSavedClipState-record pr. q, men udskyder den dyre del, til frame første gang ændrer clippen. Recorden holder region-handlen, den stakdybde, den tilhører, den device context, den blev taget fra, og et Captured-flag. DevPushState udfylder kun dybden og DC og vokser frame-arrayet ved at fordoble fra 16, så en content stream fyldt med q 1 0 0 1 x y cm ... Q allokerer slet intet GDI-objekt. Operatorer, der er ved at ændre clipping — altså path-maling med en afventende W eller W*, n-operatoren, pattern fills og form-entry — kalder 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; // allerede gemt, eller ikke vores
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 betyder slet ingen 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 fjerner clippen
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
Den målte omkostning ved den ivrige version er grunden til, at dette design findes. Den første korrekte implementering oprettede og læste en GDI-region ved hver q, og på sider, der mest bestod af numeriske transforms, brugte renderer-trådene tiden på at konkurrere om GDI-region-objekter i stedet for at rasterisere. Den parallelle render-pipeline faldt fra sin forventede gevinst til omtrent 1,13 til 1,20 gange single-threaded throughput og fejlede 1,5-ganges-speedup-gaten i benchmark-suitten. Med lazy capture og genbrugt frame-kapacitet består samme benchmark den originale 1,5-ganges-gate igen. Lille TrueType glyph-antialiasing landede i samme release og var den oplagte mistænkte, men regressionen sporedes tilbage til region-allokering, hvilket er en god påmindelse om at måle, før man bebrejder den nyeste feature
Hvor er grænserne for denne tilgang?
Den gemte clip er en GDI-region i device pixels, så den er eksakt for den bitmap, der renderes, og meningsløs for ethvert andet target. Derfor registrerer hver frame sin device context, og DevPopState skipper gendannelsen, når DC har skiftet, for eksempel mens en transparency group renderer ind i sit eget lag-bitmap. GetClipRgn, der returnerer nul, er et legitimt resultat, der betyder ingen clip, og at gendanne den med SelectClipRgn(FDC, 0) er det, der korrekt fjerner en clip, der ikke fandtes ved det matchende q. På tekstsiden retter fixet, hvor hver glyph lander, men den opfinder ikke bredder: udelader en font sit /Widths-array, og er det embeddede program utilgængeligt, er advance stadig kun så god som width-fallbacken. Når du regression-tester dette område, så behold mindst ét fixture med Tf 1 og en skaleret Tm, ét med ikke-nul Tz og Tw og ét med en clip inde i q ... Q efterfulgt af indhold uden for den, for ingen af dem dukker op i dokumenter genereret af biblioteket selv
Driver du rendereren fra applikationskode, ændrer intet sig i det kaldemønster, der beskrives i at rendere en PDF-side til en bitmap, og sider, der tidligere viste smear'ede linjer eller beskåret indhold, skulle simpelthen render korrekt på 2.754.0 og senere. Detaljer om komponenten, understøttede Delphi- og C++Builder-versioner og licensering findes på produktsiden for HotPDF Delphi PDF Component