Tekninen artikkeli

PDFium-matriisin prepend vs append Delphissä: pivotointi

PDF-affiinit matriisit käyttävät ISO 32000-1 §8.3.3:n rivivektorikäytäntöä, jossa piste kertoo matriisin vasemmalta: point' = point * M. PDFium Componentissa Delphille ja C++Builderille tuo yksi tosiasia kiinnittää koko TPdfMatrix:n API-pinnan: Multiply liittää perään, joten M := M * Op, kun taas PreMultiply liittää eteen, joten M := Op * M

Jokainen klassinen muunnosbugi juontaa siihen, että tuo lause on muistettu takaperin. Vesileima, joka pyörii siististi testitiedostossasi ja päätyy puoliksi sivun ulkopuolelle asiakastiedostossa. Pienoiskuva, joka tulee ulos pyöritettynä kahdesti, koska sivu kantoi jo neljänneskierroksen. Leima, jonka siirtymä on täydellinen A4:llä ja ajautuu pois Letterillä. Mikään niistä ei ole renderöintibugi; ne ovat kertolaskujärjestysbugeja, ja ne kaikki korjautuvat heti, kun pystyt sanomaan ääneen, missä avaruudessa jokainen toiminto on kirjoitettu

Rivivektorikäytäntö, joka asettaa säännöt

TPdfMatrix tallentaa kuusi spesifikaationimettyä elementtiä ja soveltaa niitä täsmälleen niin kuin formaatti ne määrittelee, joten itse muunnos on se, mistä päättely alkaa. TPdfMatrix.TransformPoint laskee x' = x*a + y*c + e ja y' = x*b + y*d + f, mikä on kuuselementtinen muoto, jonka ISO 32000-1 §8.3.4 määrittelee cm-operaattorille, joka yhdistää matriisin nykyiseen muunnosmatriisiin. Pari (a, b) on ensimmäinen rivi, (c, d) toinen, ja (e, f) siirtymärivi. OpenGL:stä tai lineaarialgebran kurssilta opitut sarakevektoritottumukset johtavat sinut harhaan täällä, ja ne johtavat harhaan hiljaa, koska väärän järjestyksen matriisi on silti täysin pätevä matriisi. Lue koostetta rivikäytännössä vasemmalta oikealle, ja soveltamisjärjestys selviää ilmaiseksi: koska point * (M * Op) on yhtä kuin (point * M) * Op, liitetty toiminto vaikuttaa koordinaatteihin, jotka olemassa oleva matriisi on jo tuottanut, eli sivuavaruudessa, kun taas eteen liitetty toiminto vaikuttaa ennen kuin olemassa oleva matriisi ajaa, objektin omassa syöteavaruudessa

var
  M: TPdfMatrix;
  Pt: FS_POINTF;
begin
  M := TPdfMatrix.Create;                // identity
  try
    // Append order: each call acts on what the previous calls produced.
    M.Scale(0.5, 0.5);                   // M := M * S   half size
    M.Rotate(90);                        // M := M * R   clockwise, degrees
    M.Translate(300, 400);               // M := M * T   then move on the page

    Pt := M.TransformPoint(0, 0);        // x*a + y*c + e, x*b + y*d + f
  finally
    M.Free;
  end;
end;

TPdfMatrix.Rotate on oletusarvoisesti myötäpäivään ja asteina, ja ACounterClockwise ja AAngleInRadians ovat saatavilla, kun lähdedatasi on merkitty toiseen suuntaan. Vain-luku-a:sta f:ään -ominaisuudet ja Handle-ominaisuus antavat sinulle takaisin raa'an FS_MATRIX:n, mitä FPDFPageObj_SetMatrix haluaa. Mikään luokassa ei piilota noita kuutta lukua sinulta, ja se on tarkoituksellista: kun muunnos käyttäytyy väärin, a:sta f:ään tulostaminen on nopein diagnoosi, joka sinulla on

Miksi siirtymän eteen liittäminen tarvitsee lineaarisen osan?

Koska eteen liitetty siirto on kirjoitettu matriisin syöteavaruudessa, ja se täytyy kuljettaa nykyisen lineaarisen osan läpi, ennen kuin se voi liittyä siirtymäriviin. TPdfMatrix.PreTranslate laskee siis e := dx*a + dy*c + e ja f := dx*b + dy*d + f. Perään liittäminen on helppo suunta: TPdfMatrix.Translate on kirjoitettu sivuavaruudessa, jossa mitään ei tarvitse muuntaa, joten se vain lisää dx:n e:hen ja dy:n f:ään. Kuka tahansa, joka "optimoi" PreTranslate:n kahdeksi yhteenlaskuksi, on juuri poistanut kierron ja skaalauksen siirrosta

M := TPdfMatrix.Create;
try
  M.Rotate(90);              // a=0, b=-1, c=1, d=0

  M.Translate(10, 0);        // append: e := e + 10
                             // -> 10 points to the right on the page

  M.Reset;
  M.Rotate(90);
  M.PreTranslate(10, 0);     // prepend: e := 10*a + 0*c + e  (unchanged)
                             //          f := 10*b + 0*d + f  (f - 10)
                             // -> 10 points along the stamp own x axis,
                             //    which after the turn points down the page
finally
  M.Free;
end;

Sama epäsymmetria kulkee läpi skaalausparin, ja se kannattaa tietää, mitkä elementit kumpikin koskettaa, ennen kuin debuggaat yhtä kello kolmelta yöllä. TPdfMatrix.PreScale kertoo rivejä, skaalaten a:n ja b:n scaleX:lla ja c:n ja d:n scaleY:llä, ja se jättää siirtymän rauhaan, koska siirto on jo tapahtunut alavirrassa. Liittävä TPdfMatrix.Scale kertoo sen sijaan sarakkeita, ottaen a:n, c:n, e:n scaleX:lla ja b:n, d:n, f:n scaleY:llä, joten olemassa oleva siirtymä skaalautuu kaiken muun mukana. Molemmat ovat yksitarkoituksisia polkuja, jotka ohittavat yleisen kuuselementtisen tulon, ja molemmat säilyttävät yleisen muodon koostumissemantiikan täsmälleen

Minne kaksi siirtymää menevät pivot-kierrossa?

Toiminnon ympärille, ei koko matriisin ympärille, ja tuossa järjestyksessä. TPdfMatrix.RotateAt liittää perään Translate(-pivot):in, sitten kierron, sitten Translate(+pivot):in, mikä rivivektorikäytännön alla koostuu muotoon Translate(-pivot) * Op * Translate(pivot). Tuo sekvenssi on se, mikä pitää pivotin kiinteänä uuden toiminnon alla samalla kun se yhä antaa olemassa olevan matriisin tuottaa koordinaattinsa ensin ja välittää ne eteenpäin. Kirjoita pari toisin päin, kuten se olisi oikein sarakevektorikirjastossa, ja objekti kiertää origon ympäri sen sijaan, että se pyörisi paikallaan, mikä on täsmälleen se, miten keskitetty vesileima päätyy leikkauslaatikon ulkopuolelle

procedure RotateStampAboutPageCenter(AObj: FPDF_PAGEOBJECT;
  const AAngleDegrees, APageWidth, APageHeight: Single);
var
  M: TPdfMatrix;
  Raw: FS_MATRIX;
begin
  if not FPDFPageObj_GetMatrix(AObj, Raw) then
    raise Exception.Create('Page object carries no matrix');
  M := TPdfMatrix.Create(Raw);
  try
    // Appends Translate(-pivot) * Rotate * Translate(+pivot) in one call.
    M.RotateAt(AAngleDegrees, APageWidth / 2, APageHeight / 2);
    Raw := M.Handle;
    FPDFPageObj_SetMatrix(AObj, Raw);
  finally
    M.Free;
  end;
end;

Sama koostuminen tukee ScaleAt:ia, SkewAt:ia, HorizontalFlipAt:ia, VerticalFlipAt:ia ja CentralFlipAt:ia, joten heti kun luotat kuvioon kierrolle, voit luottaa siihen muillekin. TPdfMatrix.CentralFlip ansaitsee erityismaininnan: se negatoi kaikki kuusi elementtiä antaakseen sinulle 180 asteen käännöksen ilman minkäänlaista trigonometriaa, mikä tarkoittaa ei cos:ia arvolle, jonka olisi pitänyt olla täsmälleen nolla, eikä kertyvää ajautumaa, kun sovellat sitä silmukassa. Jos sijoitat toistuvia merkkejä yhden kääntämisen sijaan, itse sijoittelun mekaniikka käsitellään artikkelissa uudelleenkäytettävistä sivuleimoista Form XObjectien avulla, ja täkäläinen matriisityö istuu suoraan sen päällä

Mitä TryDecompose kertoo sinulle matriisista?

TPdfMatrix.TryDecompose raportoi siirtymän, skaalan, kierron, leikkauksen, determinantin ja peilauslipun skaala-sitten-kierto-käytännön alla, ja se raportoi ne tarpeeksi rehellisesti ollakseen hyödyllinen päätöksille eikä vain lokitukselle. ScaleX tulee ensimmäisen rivin pituudesta, Sqrt(a*a + b*b), joten se on aina positiivinen. ScaleY on sitten Determinant / ScaleX, mikä tekee siitä etumerkillisen. Kierto tulee ArcTan2(-b, a):sta asteina, ja leikkaus kahden rivin pistetulosta normalisoituna molemmilla skaaloilla

Tuo etumerkki ScaleY:ssä on osa, jonka ihmiset poistavat, ja sen poistaminen on todellinen bugi eikä kosmeettinen. Negatiivinen determinantti tarkoittaa, että matriisi sisältää peilauksen. Pakota molemmat skaalauskertoimet positiivisiksi saadaksesi luvut näyttämään siistimmiltä, ja olet heittänyt peilauksen pois, joten hajotelmasta uudelleenrakennettu matriisi tulee takaisin peilikuvana: teksti lukee takaperin, skannattu sivu kääntyy, tuotu logo katsoo väärään suuntaan. IsReflected-kenttä on olemassa, jotta sinun ei koskaan tarvitse päätellä sitä. Tämä on myös tarkistus, joka estää klassisen kaksinkertaisen kierron, jossa koodi lisää näyttökierron sivulle, joka jo kantaa sellaisen; tuon ongelman katseluohjelmapuolen versio käsitellään artikkelissa pienoiskuvan sovituksesta, zoomauksesta ja kaksinkertaisesta kierrosta

var
  D: TPdfMatrixDecomposition;
begin
  if M.TryDecompose(D) then
  begin
    // D.ScaleX is always positive; D.ScaleY carries the determinant sign.
    if D.IsReflected then
      Log('mirrored, ScaleY = %.3f', [D.ScaleY]);

    if Abs(D.RotationDegrees) > 0.5 then
      SkipDisplayRotation;      // the object already carries its own turn
  end
  else
    UseIdentityFallback;        // near-singular or non-finite: no answer
end;

Yhden suorakulmion sovittaminen toiseen ilman arvailua

TPdfMatrix.TryCreateRectMapping rakentaa lähde-kohde-matriisin puolestasi ja ottaa TPdfMatrixFitMode-arvon pmfStretch, pmfContain tai pmfCover. Se normalisoi molemmat suorakulmiot ensin, koska PDF-suorakulmioiden ei vaadita saapuvan vasen alle oikean tai alaosa alle yläosan, ja johtaa sitten itsenäiset X- ja Y-skaalat: pmfStretch pitää ne itsenäisinä, pmfContain ottaa pienemmän ja keskittää postilaatikkoreunuksen, pmfCover ottaa suuremman ja keskittää rajauksen. Rinnakkainen MapRectToRect liittää saman kartoituksen olemassa olevaan matriisiin, ja NewRectMapping nostaa EPdfMatrixError:n siinä, missä Try-muoto palauttaa False:n. Tämä on alkeisosanen jokaisen solusijoittelun alla artikkelissa N-up-taittelusta ja sivujen uudelleenjärjestelystä, jossa jokaisen lähdesivun täytyy laskeutua laskettuun soluun ilman, että johdat aritmetiikan uudelleen jokaista asettelua kohti

Degeneroituneet matriisit ja rehellinen epäonnistumispolku

Äärelliset syötteet eivät takaa äärellistä tulosta, joten sovituskoodi laskee Double:na ja tarkastaa sitten kavennetun Single-ehdokkaan uudelleen äärellisyyden varalta ennen sen julkaisemista; kartoitusta, joka sisältää äärettömyyden, ei koskaan anneta takaisin ikään kuin se olisi pätevä. Sama kuri hallitsee inversiota. TPdfMatrix.TryGetInverse hylkää matriisin suhteellisella kynnysarvolla, vertaamalla determinanttia epsiloniin kerrottuna suurimman lineaarisen elementin neliöllä eikä kiinteään vakioon, mikä on se, mikä pitää testin merkityksellisenä riippumatta siitä, ovatko yksikkösi pisteitä vai mikrometrejä. TryDecompose vetäytyy samalla tavalla, kieltäytyen kun ensimmäisen rivin pituus tai johdettu ScaleY putoaa epsilonin tasolle tai sen alle

Valitse epäonnistumistyyli, joka sopii kutsupaikkaan sen sijaan, että kääritset kaiken try-except:iin tottumuksesta. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds ja TryCreateRectMapping palauttavat False:n ja jättävät kohteensa koskemattomiksi, mikä sopii osumatestaukseen ja objektikohtaisiin silmukoihin, joissa degeneroitunut objekti tulisi ohittaa, ei olla kohtalokas. Invert, InverseCopy, InverseTransformPoint, MapRectToRect ja TransformBounds nostavat sen sijaan EPdfMatrixError:n, mikä sopii asetuskoodiin, jossa singulaarinen matriisi tarkoittaa, että kutsuja laski jotain väärin. Eräajotyölle TransformPoints ja TransformRects varaavat tulostaulukkonsa täsmälleen kerran, TransformPointsInPlace ja TransformRectsInPlace käyttävät uudelleen omaa varastoasi, ja TryTransformBounds kerää rajauslaatikon yhdessä läpikäynnissä sen sijaan, että se materialisoisi muunnetut pisteet ensin

Mikään tästä ei ole eksoottista matematiikkaa. Se on yksi käytäntö, sovellettuna johdonmukaisesti, API nimettynä niin, että käytäntö on näkyvissä kutsupaikassa: Multiply ja tavalliset verbit liittävät perään, Pre-perhe liittää eteen, At-perhe sulkee toiminnon pivot-parinsa sisään. Kirjoita järjestys muistiin kommenttina minkä tahansa koostetta rakentaessasi, koska koodi, joka lukee oikein tänään, on koodi, jonka joku kääntää ympäri kuuden kuukauden kuluttua. Täydellinen TPdfMatrix-viite, yhdessä sivuobjekti- ja renderöinti-API:en kanssa, joita nämä muunnokset syöttävät, elää PDFium Component -komponentin mukana Delphille ja C++Builderille