Teknisk artikkel

PDF-tekstavstand og q/Q-klippgjenoppretting i Delphi

Siderendereren i HotPDF Delphi Component flytter nå teksten fremover ved å beregne hver glyff-forskyvning i tekstrom, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th slik ISO 32000-1 §9.4.4 definerer det, og flytter så tekstmatrisen gjennom sin lineære del med HPDFTranslateTextMatrix. Klipping lagres per q-ramme og gjenopprettes på Q, men en GDI-region fanges bare inn når den rammen faktisk endrer klippet. Begge fiksene landet i HotPDF 2.754.0, og begge kom fra sider i virkeligheten som rendret med sammenklemte ord eller klippregioner som lekket forbi sin Q. Den første feilen er aritmetikk som ser riktig ut helt til en produsent skriver sin fontstørrelse inn i matrisen. Den andre er en korrekthetsfiks som nesten kostet oss den parallelle render-hastighetsgevinsten, og måten vi fikk hastigheten tilbake på, er verdt å kjenne hvis du skriver noen GDI-basert PDF-enhet

Hvorfor klemmer tekst seg sammen i en klump når en PDF bruker Tf 1?

Fordi den gamle avstandskoden la en tekstromsavstand rett til translasjonskomponenten av Tm, som om tekstrom og brukerrom alltid hadde samme skala. Masse produsenter i virkeligheten setter fontstørrelsen til 1 med Tf og bærer den ekte størrelsen i tekstmatrisen. Med /F1 1 Tf og 12 0 0 12 72 700 Tm rykker en glyff på 500 enheter 0,5 i tekstrom, som er 6 punkter på siden når Tm skalerer den. Den gamle rendereren utførte Tm.e := Tm.e + Adv og flyttet pennen 0,5 punkter. Hver glyff landet en tolvtedel av et tegn etter den forrige, så en linje med brødtekst rendret som en mørk flekk ved venstre marg, mens samme fil så perfekt ut i alle andre visningsprogrammer

Hvorfor tekst klemmer seg sammen under Tf 1 i HotPDF-rendereren: med 12 0 0 12 72 700 Tm må en glyff på 500 enheter rykke 0,5 tekstrom-enheter, som Tm skalerer til 6 punkter, mens den gamle koden la 0,5 rett til Tm.e og rendret en linje med brødtekst som en flekk på en tolvtedel av et tegn per glyff
Produsenter som enkode fontstørrelsen i tekstmatrisen fikk hver glyff til å lande en tolvtedel av et tegn etter den forrige, en defekt usynlig på bibliotekets eget resultat
// Innholdsstrøm fra en produsent som enkode 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 avstand (forenklet): avstand lagt til Tm.e som om den var brukerrom
Adv := W * FontSize / 1000;                  // 0,5 for en glyff på 500 enheter
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // Th kun på bredden
Adv := Adv + CharSpace;                      // Tc ikke skalert av Th
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // Tw feilaktig skalert av Tfs
Tm.e := Tm.e + Adv;                          // ignorerer Tm.a, Tm.b, Tm.c, Tm.d

// Gammel TJ-justering: ingen Th, og igjen bare Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;

Tm.e-snarveien var ikke den eneste defekten i den blokken. Ordmellomrom Tw uttrykkes i uskalerte tekstrom-enheter, likevel multipliserte den gamle koden det med FontSize / 1000, så under Tf 12 mistet en justert linje nesten alt sitt mellomrom mellom ord. Horisontal skalering Th anvendtes på glyffbredden men ikke på Tc eller Tw, og TJ-kerningjusteringen hoppet over den helt. Den ikke-malende stien som flytter frem modus 3 usynlig tekst, den typen OCR-tekstlag bruker, og tekst inne i skjult optional content, bar en privat kopi av samme aritmetikk, så alt tegnet etter et usynlig løp startet fra feil posisjon. Teksttilstandsfeil i en renderer feiler sjelden høyt: som operandindeks- og ressursnavn-feilene som en gang nullstilte Tc, Tw og Tz uten én eneste feilmelding, produserte disse plausige sider på bibliotekets eget resultat og brøt bare på filer fra andre produsenter

Hvordan definerer ISO 32000-1 §9.4.4 glyffavstanden?

ISO 32000-1 §9.4.4 definerer avstanden fullt ut i tekstrom og anvender den på tekstmatrisen som en translasjonsmatrise, så svaret er å beregne tx først og la Tm gjøre skaleringen, rotasjonen og skjevheten. For horisontal skriving er tx lik ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, der w0 er glyffbredden i tusendedeler av en em, Tj er TJ-justeringen, og Th er Tz delt på 100. Den nye Tm-en er [1 0 0 1 tx 0] × Tm, som i HotPDF er hjelperen HPDFTranslateTextMatrix: den legger til X og Y gjennom matrisekoeffisientene a, b, c og d i stedet for å skrive til e og f direkte. Per §9.3.3 anvendes Tw bare på enkeltbyte-tegncoden 32, så flerbyte CID-koder plukker aldri opp ordmellomrom på den horisontale stien. Samme hjelper driver nå Td, TD, T*, '- og "-operatorene, TJ-justeringer og den skjulte tekststien, noe som betyr at én enkelt funksjon eier regelen

ISO 32000-1 9.4.4 glyffavstand i HotPDF-rendereren: tx beregnet i tekstrom fra w0, Tj, Tfs, Tc, Tw og Th, så anvendt gjennom HPDFTranslateTextMatrix slik at forskyvningen passerer over matrisekoeffisientene a, b, c og d, og Td, TD, TJ og den skjulte tekststien deler én regel
Å legge avstanden rett til Tm.e virker bare når tekstrom er lik brukerrom; å rute den gjennom matrisekoeffisientene holder skalert, rotert og skjev tekst korrekt
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 glyffavstand, 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 tekstrom, uskalert
Adv := Adv * State.Text.HorizScale / 100;    // Th gjelder hele summen
HPDFTranslateTextMatrix(Tm, Adv, 0);

// TJ-tallelement: samme rom, samme Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);

Glyffplassering måtte følge samme logikk. Når ingen innebygd omriss er tilgjengelig og rendereren faller tilbake til GDI TextOutW, bygger den nå den fulle glyffmatrisen fra CTM × Tm × rise × em-skala, inkludert Th, og installerer den med SetWorldTransform i GM_ADVANCED-modus inne i et SaveDC / RestoreDC-par. GDI-fonten opprettes ved en fast 1000-enhets høyde og transformasjonen gjør størrelsessettingen, så rotert og skjev tekst beholder sin orientering i stedet for å tegnes oppreist på et transformert origopunkt. Vertikal skrivemodus er den eneste bevisste asymmetrien: en WMode 1-font rykker nedover y-aksen med sin vertikale metrikk, og horisontal skalering gjelder ikke den aksen

Hva lagrer egentlig q/Q i en PDF-grafikktilstand?

ISO 32000-1 §8.4.2 lister den gjeldende klippstien som del av grafikktilstanden, så Q må gjenopprette klippet nøyaktig slik det var ved den matchende q, ikke bare de numeriske parametrene. HotPDF holdt allerede en grafikktilstandsstakk med CTM, farger, linjeparametre og teksttilstand, men GDI holder klippet i enhetskonteksten, utenfor den stakken. En kopi av den numeriske tilstanden gjenopprettet derfor alt unntatt klippet, og et klipp installert med W n inne i en q ... Q-blokk holdt seg beskjærende enhver senere operasjon på siden. Form XObjects la til en andre rute til samme feil, fordi §8.10 gir en form en implisitt lagring og gjenoppretting rundt innholdet sitt, og virkelighetsformsinnhold etterlater noen ganger sine egne q-operatorer ubalanserte selv om spesifikasjonen krever at de parer opp. Rendereren kaller nå CaptureClipBeforeChange og SaveDC før den kjører en form, og etter at formen er ferdig kaster den enhver lagret region dypere enn inngangsdypden og kaller RestoreDC, så hver lagret HRGN har nøyaktig én frigjøringssti

Lat klippinnfanging med THPDFSavedClipState

Fiksen som shipper lagrer én THPDFSavedClipState-post per q, men utsetter den dyre delen til rammen først endrer klippet. Posten holder regionhandleet, stakkdypden den tilhører, enhetskonteksten den ble tatt fra og et Captured-flagg. DevPushState fyller bare inn dypden og DC-en og vokser ramme-arrayen ved å doble fra 16, så en innholdsstrøm full av q 1 0 0 1 x y cm ... Q allokerer intet GDI-objekt i det hele tatt. Operatorer som er i ferd med å endre klipping, altså stmaling med en ventende W eller W*, n-operatoren, mønstrefyll og forminngang, kaller CaptureClipBeforeChange først

Lat GDI-klippinnfanging i HotPDF-rendereren: DevPushState registrerer bare dypde og DC per q, CaptureClipBeforeChange leser regionen rett før W, n eller forminngang endrer klipping, DevPopState gjenoppretter og sletter den på Q, og den ivrige versjonen som fanget inn på hver q falt parallellgjennomstrømningen til omtrent 1,15 ganger enkelttrådet
Å opprette en GDI-region på hver q sulter rendrertrådene, så innfanging skjer nå bare når en operator er i ferd med å endre klipping, og 1,5 ganger-hastighetsporten går gjennom igjen
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 lagret, eller ikke vår
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // 0 betyr intet klipp i det hele tatt
  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 klippet
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

Den målte kostnaden av den ivrige versjonen er grunnen til at dette designet finnes. Den første korrekte implementeringen opprettet og leste en GDI-region på hver q, og på sider hovedsakelig bestående av numeriske transformasjoner brukte renderer-trådene tiden sin på å konkurrere om GDI-regionobjekter i stedet for å rasterisere. Den parallelle render-pipelinen falt fra sin forventede gevinst til omtrent 1,13 til 1,20 ganger enkelttrådet gjennomstrømning og feilet 1,5 ganger-hastighetsporten i benchmark-suiten. Med lat innfanging og gjenbrukt rammekapasitet går samme benchmark gjennom den opprinnelige 1,5 ganger-porten igjen. Liten TrueType glyff-antialiasing landet i samme utgivelse og var den opplagte mistenkte, men regresjonen sporet tilbake til region-allokering, noe som er en god påminnelse om å måle før du skylder på den nyeste funksjonen

Hvor er grensene for denne tilnærmingen?

Det lagrede klippet er en GDI-region i enhetspiksler, så det er eksakt for bitmapet som rendres og meningsløst for ethvert annet mål. Det er grunnen til at hver ramme registrerer sin enhetskontekst, og DevPopState hopper over gjenopprettingen når DC-en har endret seg, for eksempel mens en transparansgruppe rendrer inn i sitt eget lagbitmap. GetClipRgn som returnerer null, er et legitimt resultat som betyr intet klipp, og å gjenopprette det med SelectClipRgn(FDC, 0) er det som korrekt fjerner et klipp som ikke fantes ved den matchende q. På tekstsiden korrigerer fiksen hvor hver glyff går, men den oppfinner ikke bredder: hvis en font utelater /Widths-arrayen sin og det innebygde programmet er utilgjengelig, er avstanden fortsatt bare så god som bredde-fallbacken. Når du regresjonstester dette området, behold minst én fixtur med Tf 1 og en skalert Tm, én med ikke-null Tz og Tw, og én med et klipp inne i q ... Q fulgt av innhold utenfor det, for ingen av dem dukker opp i dokumenter generert av biblioteket selv

Hvis du driver rendereren fra applikasjonskode, endres ingenting i kallmønsteret beskrevet i å rendre en PDF-side til et bitmap, og sider som tidligere viste utsmurte linjer eller beskåret innhold, bør rett og slett rendre korrekt på 2.754.0 og senere. Detaljer om komponenten, støttede Delphi- og C++Builder-versjoner og lisensiering finnes på produktsiden for HotPDF Delphi PDF Component