Tekninen artikkeli

PDF-tekstin siirtymä ja q/Q-clipin palautus renderöijässä

HotPDF Delphi Componentin sivurenderoija etenee nyt tekstissä laskemalla jokaisen glyyfin siirtymän tekstitilassa, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, niin kuin ISO 32000-1 §9.4.4 määrittelee, ja liikuttamalla sitten tekstimatriisia sen lineaarisen osan kautta HPDFTranslateTextMatrixilla. Clippaus tallennetaan per q-kehys ja palautetaan Qssa, mutta GDI-region kaapataan vain, kun kyseinen kehys oikeasti muuttaa clippiä. Molemmat korjaukset saapuivat HotPDF 2.754.0:aan, ja molemmat tulivat oikeista sivuista, jotka renderöityivät lomittain kasaantunein sanoin tai vuotavilla clip-regioneilla Qnsa ohi. Ensimmäinen bugi on aritmetiikkaa, joka näyttää oikealta siihen asti, kunnes tuottaja kirjoittaa fonttikokonsa matriisiin. Toinen on korrektiuskorjaus, joka melkein maksoi meille rinnakkaisen renderöinnin nopeutuksen, ja tapa, jolla saimme nopeuden takaisin, on tiedon arvoinen, jos kirjoitat mitään GDI-pohjaista PDF-laitetta

Miksi teksti lomaantuu rykelmäksi, kun PDF käyttää arvoa Tf 1?

Koska vanha siirtymäkoodi lisäsi tekstitilan etäisyyden suoraan Tmin translaatiokomponenttiin, ikään kuin tekstitilalla ja käyttäjätilalla olisi aina sama skaala. Paljon oikeita tuottajia asettaa fonttikooksi 1 Tfllä ja kantaa oikeaa kokoa tekstimatriisissa. Arvoilla /F1 1 Tf ja 12 0 0 12 72 700 Tm 500 yksikön levyinen glyyfi siirtyy 0,5 tekstitilan yksikköä, mikä on 6 pistettä sivulla, kun Tm skaalaa sen. Vanha renderoija suoritti lausekkeen Tm.e := Tm.e + Adv ja siirsi kynää 0,5 pistettä. Jokainen glyyfi laskeutui yhden kahdennentoistaosan merkin verran edellisen jälkeen, joten runkotekstin rivi renderöityi tummana tahmana vasemmassa reunassa, kun taas sama tiedosto näytti täydelliseltä kaikissa muissa katselimissa

Miksi teksti lomaantuu arvolla Tf 1 HotPDF:n renderoijassa: arvoilla 12 0 0 12 72 700 Tm 500-yksikköisen glyyfin on siirryttävä 0,5 tekstitilan yksikköä, jonka Tm skaalaa 6 pisteeksi, kun taas vanha koodi lisäsi 0,5:n suoraan Tm.een ja renderöi runkotekstin rivin tahmana, yhden kahdennentoistaosan merkkiä per glyyfi
Tuottajat, jotka koodaavat fonttikoon tekstimatriisiin, saivat jokaisen glyyfin laskeutumaan yhden kahdennentoistaosan merkin verran edellisen jälkeen — vika, joka on näkymätön kirjaston omassa tuotoksessa
// Sisältöstream tuottajalta, joka koodaa koon Tm:ään, ei Tf:ään:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// Vanha siirtymä (yksinkertaistettu): etäisyys lisätty Tm.een ikään kuin se olisi käyttäjätilaa
Adv := W * FontSize / 1000;                  // 0,5 500-yksikköiselle glyyfille
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // Th vain leveydelle
Adv := Adv + CharSpace;                      // Tc:tä ei skaalata Th:lla
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // Tw väärin skaalattuna Tfs:llä
Tm.e := Tm.e + Adv;                          // ohittaa Tm.a:n, Tm.b:n, Tm.c:n ja Tm.d:n

// Vanha TJ-säätö: ei Th:ta, ja taas vain Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;

Tm.e-oikotie ei ollut ainoa vika kyseisessä lohkossa. Sanaväli Tw ilmaistaan skaalaamattomissa tekstitilan yksiköissä, mutta vanha koodi kertoi sen arvolla FontSize / 1000, joten arvolla Tf 12 tasattu rivi menetti lähes koko sanavälinsä. Vaakaskaalaus Th kohdistui glyyfin leveyteen mutta ei Tchen tai Twhen, ja TJ-kerningssäätö ohitti sen kokonaan. Ei-maalaava polku, joka siirtää renderöintitilan 3 näkymätöntä tekstiä — sellaista, jota OCR-tekstikerrokset käyttävät — sekä piilotetun valinnaisen sisällön sisällä oleva teksti kantoi saman aritmetiikan yksityistä kopiota, joten mikä tahansa näkymättömän ajon jälkeen piirretty alkoi väärästä sijainnista. Tekstitilan bugit renderoijassa kaatuvat harvoin äänekkäästi: kuten operandi-indeksin ja resurssinimen bugit, jotka kerran nollasivat Tcn, Twn ja Tzn ilman ainuttakaan virhettä, nämä tuottivat uskottavia sivuja kirjaston omassa tuotoksessa ja rikoivat vain muiden tuottajien tiedostoissa

Miten ISO 32000-1 §9.4.4 määrittelee glyyfin siirtymän?

ISO 32000-1 §9.4.4 määrittelee siirtymän täysin tekstitilassa ja soveltaa sitä tekstimatriisiin translaatiomatriisina, joten vastaus on laskea tx ensin ja antaa Tmin hoitaa skaalaus, kierto ja vinouma. Vaakakirjoituksella tx on ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, jossa w0 on glyyfin leveys tuhannesosina em:stä, Tj on TJ-säätö, ja Th on Tz jaettuna 100:lla. Uusi Tm on [1 0 0 1 tx 0] × Tm, mikä HotPDF:ssä on apurifunktio HPDFTranslateTextMatrix: se lisää X:n ja Y:n matriisikertoimien a:n, b:n, c:n ja d:n kautta kirjoittamatta suoraan ehen ja fhen. Kohdan §9.3.3 mukaan Tw koskee vain yksitavuista merkintäkoodia 32, joten monitavuiset CID-koodit eivät koskaan poimi sanaväliä vaakapolulla. Sama apurifunktio ajaa nyt Tdn, TDn, T*n, '- ja "-operaattorit, TJ-säädöt ja piilotetun tekstipolun, mikä tarkoittaa, että yksi funktio omistaa säännön

ISO 32000-1 9.4.4:n glyyfinsiirtymä HotPDF:n renderoijassa: tx lasketaan tekstitilassa arvoista w0, Tj, Tfs, Tc, Tw ja Th, ja se sovelletaan HPDFTranslateTextMatrixin kautta, jolloin siirtymä kulkee matriisikertoimien a:n, b:n, c:n ja d:n yli ja Td, TD, TJ ja piilotettu tekstipolku jakavat yhden säännön
Siirtymän lisääminen suoraan Tm.een toimii vain, kun tekstitila vastaa käyttäjätilaa; reitittäminen matriisikertoimien kautta pitää skaalatun, kierretyn ja väännetyt tekstin oikeana
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;

// Vaakaglyyfin siirtymä, 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 tekstitilassa, skaalaamaton
Adv := Adv * State.Text.HorizScale / 100;    // Th koskee koko summaa
HPDFTranslateTextMatrix(Tm, Adv, 0);

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

Glyyfien sijoittelun piti seurata samaa logiikkaa. Kun upotettua ääriviivaa ei ole saatavilla ja renderoija putoaa GDI:n TextOutWille, se rakentaa nyt täyden glyyfimatriisin kaavasta CTM × Tm × rise × em-skaala, mukaanlukien Th, ja asentaa sen SetWorldTransformilla GM_ADVANCED-tilassa SaveDC / RestoreDC-parin sisällä. GDI-fontti luodaan kiinteällä 1000 yksikön korkeudella ja muunnos tekee koon, joten kiertynyt ja vääntynyt teksti säilyttää suuntansa sen sijaan että piirrettäisiin pystyssä muunnetusta alkuperäpisteestä. Pystykirjoitustila on ainoa tahallinen epäsymmetria: WMode 1 -fontti siirtyy y-akselia alaspäin pystymetriikallaan, ja vaakaskaalaus ei koske kyseistä akselia

Mitä q/Q oikeastaan tallentaa PDF:n grafiikkatilaan?

ISO 32000-1 §8.4.2 luettelee nykyisen clipauspolun osaksi grafiikkatilaa, joten Qin on palautettava clip täsmälleen sellaisena kuin se oli vastaavassa qssa, ei vain numeerisia parametreja. HotPDF piti jo grafiikkatilapinoa, jossa on CTM, värit, viivaparametrit ja tekstitila, mutta GDI pitää clipin laitekontekstissa, tuon pinon ulkopuolella. Kopio numeerisesta tilasta palautti siksi kaiken muun paitsi clipin, ja W nilla asennettu clip q ... Q-lohkon sisällä jatkoi jokaisen myöhemmän operaation sivulla rajaamista. Form XObjectit lisäsivät toisen reitin samaan vikaan, koska §8.10 antaa formille implisiittisen tallennuksen ja palautuksen sisällön ympärille, ja oikean maailman formisisältö joskus jättää omat q-operaattorinsa epätasapainoisiksi, vaikka spesifikaatio vaatii niiden parituvan. Renderoija kutsuu nyt CaptureClipBeforeChangeia ja SaveDCia ennen formin ajamista, ja formin valmistuttua se hylkää kaikki tallennetut regionit, jotka ovat syvemmällä kuin sisääntulosyvyys, ja kutsuu RestoreDCia, joten jokaisella tallennetulla HRGN:llä on täsmälleen yksi vapautuspolku

Laiska clipin kaappaus THPDFSavedClipStateilla

Toimitettu korjaus tallentaa yhden THPDFSavedClipState-tietueen per q, mutta lykkää kallista osaa siihen asti, kun kehys ensimmäisen kerran muuttaa clippiä. Tietue pitää sisällään regionin kahvan, pinon syvyyden, johon se kuuluu, laitekontekstin, josta se otettiin, ja Captured-lipun. DevPushState täyttää vain syvyyden ja DC:n ja kasvattaa kehyksen taulukkoa kaksinkertaistaen 16:sta alkaen, joten q 1 0 0 1 x y cm ... Q-kutsuilla täynnä oleva sisältöstream ei varaa yhtään GDI-objektia. Operaattorit, jotka ovat muuttamassa clippausta — eli polun maalaus odottavalla Wllä tai W*llä, n-operaattori, kuviotäytöt ja formiin saapuminen — kutsuvat ensin CaptureClipBeforeChangeia

Laiska GDI-clipin kaappaus HotPDF:n renderoijassa: DevPushState kirjaa vain syvyyden ja DC:n per q, CaptureClipBeforeChange lukee regionin juuri ennen kuin W, n tai formiin saapuminen muuttaa clippausta, DevPopState palauttaa ja poistaa sen Q:ssa, ja innokas versio, joka kaapaseli jokaisella q:lla, pudotti rinnakkaisen läpäisyn noin 1,15-kertaiseksi yksisäikeisestä
GDI-regionin luominen jokaisella q:lla nälkiytti renderöintisäikeet, joten kaappaus tapahtuu nyt vain, kun operaattori on muuttamassa clippausta, ja 1,5-kertaisen nopeutusportin tarkistus menee taas läpi
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;   // jo tallennettu, tai ei meidän
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // 0 tarkoittaa ei clippiä lainkaan
  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 poistaa clipin
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

Innokkaan version mitattu kustannus on syy, miksi tämä suunnittelu on olemassa. Ensimmäinen oikea toteutus loi ja luki GDI-regionin jokaisella qlla, ja sivuilla, jotka olivat pääosin numeerisia muunnoksia, renderöintisäikeet käyttivät aikansa GDI-regionobjekteista kilpailemiseen rasteroinnin sijaan. Rinnakkainen renderöintiputki putosi odotetusta hyödystään noin 1,13–1,20-kertaiseksi yksisäikeiseen läpäisyyn verrattuna ja kaatui 1,5-kertaisen nopeutusportin kohdalla benchmark-sarjassa. Laiskalla kaappauksella ja uudelleenkäytetyllä kehyskapasiteetilla sama benchmark menee alkuperäisen 1,5-kertaisen portin läpi taas. Pienten TrueType-glyyfien antialiasointi saapui samassa julkaisussa ja oli ilmeinen epäilty, mutta regressio jäljitettiin regionin varaukseen, mikä on hyvä muistutus mitata ennen kuin syytät uusinta ominaisuutta

Missä tämän lähestymistavan rajat ovat?

Tallennettu clip on GDI-region laitepikseleinä, joten se on täsmällinen renderöitävälle bittikartalle ja merkityksetön mille tahansa muulle kohteelle. Siksi jokainen kehys kirjaa laitekontekstinsa, ja DevPopState ohittaa palautuksen, kun DC on vaihtunut, esimerkiksi silloin kun läpinäkyvyysryhmä renderöityy omaan kerrosbittikarttaansa. GetClipRgnin palauttama nolla on laillinen tulos, joka tarkoittaa ei clippiä lainkaan, ja sen palauttaminen kutsulla SelectClipRgn(FDC, 0) on se, mikä oikein poistaa clipin, jota ei ollut olemassa vastaavassa qssa. Tekstin puolella korjaus oikaisee, minne jokainen glyyfi menee, mutta se ei keksi leveyksiä: jos fontti jättää /Widths-taulukon pois ja upotettu ohjelma ei ole saatavilla, siirtymä on yhä vain niin hyvä kuin leveysvarajärjestely. Kun regressiotestaat tätä aluetta, pidä mukana vähintään yksi fixture arvolla Tf 1 ja skaalatulla Tmillä, yksi nollasta poikkeavalla Tzllä ja Twllä ja yksi, jossa on clip q ... Q-lohkon sisällä ja sen jälkeistä sisältöä ulkopuolella, koska mikään näistä ei näy kirjaston itsensä tuottamissa asiakirjoissa

Jos ajat renderoijaa sovelluskoodista, kutsukaavassa, jonka artikkeli PDF-sivun renderöinnistä bittikartaksi kuvaa, ei muutu mitään, ja sivut, jotka aiemmin näyttivät tahrisia rivejä tai rajattua sisältöä, renderöityvät nyt yksinkertaisesti oikein versioissa 2.754.0 ja myöhemmissä. Tiedot komponentista, tuetuista Delphi- ja C++Builder-versioista ja lisensoinnista ovat HotPDF Delphi PDF Component -tuotesivulla