Tehnički članak

PDFium matrica Prepend naspram Append u Delphiju: rotacija oko pivota

PDF afine matrice koriste konvenciju vektora reda ISO 32000-1 §8.3.3, gde tačka množi matricu sleva: point' = point * M. U PDFium Component za Delphi i C++Builder ta jedna činjenica fiksira celu površinu API-ja TPdfMatrix: Multiply dodaje na kraj, pa M := M * Op, dok PreMultiply dodaje na početak, pa M := Op * M

Svaki klasičan bag transformacije se svodi na to da je ta rečenica upamćena unazad. Watermark koji se uredno rotira u vašem test fajlu, a završi napola van stranice u korisničkom fajlu. Thumbnail koji ispadne rotiran dvaput jer je stranica već nosila četvrtinu obrta. Pečat čiji je ofset savršen na A4, a skreće na Letter formatu. Nijedno od toga nisu bagovi renderovanja; to su bagovi redosleda množenja, i svi su popravljivi čim možete naglas reći u kom je prostoru svaka operacija napisana

Konvencija vektora reda koja postavlja pravila

TPdfMatrix čuva šest elemenata imenovanih po specifikaciji i primenjuje ih tačno kako ih format definiše, pa je sama transformacija mesto gde rezonovanje počinje. TPdfMatrix.TransformPoint izračunava x' = x*a + y*c + e i y' = x*b + y*d + f, što je oblik od šest elemenata koji ISO 32000-1 §8.3.4 definiše za operator cm koji konkateniše matricu na trenutnu matricu transformacije. Par (a, b) je prvi red, (c, d) drugi, a (e, f) red translacije. Navike vektora kolone ponete iz OpenGL-a ili sa kursa linearne algebre će vas ovde zavesti, i zavešće vas tiho, jer matrica pogrešnog redosleda je i dalje savršeno validna matrica. Čitajte kompozit u konvenciji reda sleva nadesno i redosled primene proizlazi besplatno: pošto je point * (M * Op) jednako (point * M) * Op, dodata operacija na kraju deluje na koordinate koje je postojeća matrica već proizvela, to jest na prostor stranice, dok operacija dodata na početku deluje pre nego što se postojeća matrica izvrši, u sopstvenom ulaznom prostoru objekta

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 podrazumevano rotira u smeru kazaljke na satu i u stepenima, sa ACounterClockwise i AAngleInRadians dostupnim kada su vaši izvorni podaci označeni suprotno. Svojstva a do f samo-za-čitanje i svojstvo Handle vraćaju vam sirovu FS_MATRIX, što je ono što FPDFPageObj_SetMatrix želi. Ništa u klasi ne skriva tih šest brojeva od vas, i to je namerno: kada se transformacija ponaša loše, ispisivanje a do f je najbrža dijagnoza koju imate

Zašto dodavanje translacije na početak treba linearni deo?

Zato što je pomeraj dodat na početak napisan u ulaznom prostoru matrice, i mora biti provučen kroz trenutni linearni deo pre nego što se pridruži redu translacije. TPdfMatrix.PreTranslate zato izračunava e := dx*a + dy*c + e i f := dx*b + dy*d + f. Dodavanje na kraj je lakši smer: TPdfMatrix.Translate je napisan u prostoru stranice, gde ništa ne treba konverziju, pa samo dodaje dx na e i dy na f. Ko god „optimizuje" PreTranslate na dva sabiranja upravo je obrisao rotaciju i skaliranje iz pomeraja

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;

Ista asimetrija se provlači kroz par skaliranja, i vredi znati koje elemente svako od njih dotiče pre nego što jedan otklanjate u tri ujutru. TPdfMatrix.PreScale množi redove, skalirajući a i b sa scaleX, a c i d sa scaleY, i ostavlja translaciju na miru jer se pomeraj već desio nizvodno. Dodavanje na kraj TPdfMatrix.Scale umesto toga množi kolone, uzimajući a, c, e sa scaleX, a b, d, f sa scaleY, pa se postojeći ofset skalira sa svime ostalim. Oba su putanje jedne namene koje preskaču opšti proizvod od šest elemenata, i oba čuvaju semantiku kompozicije opšteg oblika tačno

Gde idu dve translacije u pivot rotaciji?

Oko operacije, ne oko cele matrice, i tim redosledom. TPdfMatrix.RotateAt dodaje Translate(-pivot) na kraj, zatim rotaciju, zatim Translate(+pivot), što se pod konvencijom vektora reda sastavlja kao Translate(-pivot) * Op * Translate(pivot). Ta sekvenca je ono što drži pivot fiksnim pod novom operacijom dok i dalje dozvoljava postojećoj matrici da prvo proizvede svoje koordinate i preda ih dalje. Napišite taj par obrnutim redosledom, kako bi bilo ispravno u biblioteci vektora kolone, i objekat kruži oko koordinatnog početka umesto da se vrti u mestu, što je tačno kako centriran watermark završi van crop boxa

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;

Ista kompozicija stoji iza ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, i CentralFlipAt, pa čim poverujete šablonu za rotaciju, možete mu verovati i za ostalo. TPdfMatrix.CentralFlip vredi izdvojiti: negira svih šest elemenata da vam da okret od 180 stepeni bez ikakve trigonometrije, što znači bez cos vrednosti koja je trebalo da bude tačno nula i bez akumulirajućeg driftovanja kada ga primenite u petlji. Ako postavljate ponovljene oznake umesto da okrećete jednu, mehanika samog postavljanja pokrivena je u ponovo upotrebljivim pečatima stranice sa Form XObjectima, a rad sa matricom ovde sedi direktno na tome

Šta vam TryDecompose govori o matrici?

TPdfMatrix.TryDecompose prijavljuje translaciju, skaliranje, rotaciju, smicanje, determinantu i zastavicu refleksije pod konvencijom skaliranje-pa-rotacija, i prijavljuje ih dovoljno iskreno da bude korisno za odluke, ne samo za logovanje. ScaleX dolazi iz dužine prvog reda, Sqrt(a*a + b*b), pa je uvek pozitivan. ScaleY je zatim Determinant / ScaleX, što ga čini označenim. Rotacija dolazi iz ArcTan2(-b, a) u stepenima, a smicanje iz skalarnog proizvoda ta dva reda normalizovanog obema skalama

Taj znak na ScaleY je deo koji ljudi brišu, i brisanje je stvaran bag, a ne kozmetički. Negativna determinanta znači da matrica sadrži refleksiju. Prisilite oba faktora skaliranja da budu pozitivna da bi brojevi izgledali urednije, i bacili ste refleksiju, pa se matrica rekonstruisana iz dekompozicije vrati zrcaljena: tekst se čita unazad, skenirana stranica se preokrene, uvezen logo gleda na pogrešnu stranu. Polje IsReflected postoji da to nikad ne morate pretpostavljati. Ovo je takođe provera koja sprečava klasičnu dvostruku rotaciju, gde kod dodaje obrt prikaza stranici koja ga već nosi; verzija tog problema na strani viewera obrađena je u uklapanju thumbnaila, zumu, i dvostrukoj rotaciji

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;

Uklapanje jednog pravougaonika u drugi bez pogađanja

TPdfMatrix.TryCreateRectMapping gradi za vas matricu izvor-u-cilj i uzima TPdfMatrixFitMode od pmfStretch, pmfContain, ili pmfCover. Prvo normalizuje oba pravougaonika, jer PDF pravougaonici nisu obavezni da stignu sa levim ispod desnog ili donjim ispod gornjeg, zatim izvodi nezavisne X i Y skale: pmfStretch ih drži nezavisnim, pmfContain uzima manju i centrira letterbox, pmfCover uzima veću i centrira isecanje. Prateći MapRectToRect dodaje isto mapiranje na kraj postojeće matrice, a NewRectMapping baca EPdfMatrixError tamo gde Try oblik vraća False. Ovo je primitivac ispod svakog postavljanja ćelije u N-up impoziciji i preuređivanju stranica, gde svaka izvorna stranica mora sleteti unutar izračunate ćelije bez da ponovo izvodite aritmetiku po rasporedu

Degenerisane matrice i iskrena putanja neuspeha

Konačni ulazi ne garantuju konačan rezultat, pa kod za uklapanje računa u Double, a zatim ponovo proverava suženog Single kandidata na konačnost pre nego što ga objavi; mapiranje koje sadrži beskonačnost se nikad ne vraća kao da je validno. Ista disciplina upravlja inverzijom. TPdfMatrix.TryGetInverse odbacuje matricu koristeći relativan prag, upoređujući determinantu sa epsilonom pomnoženim kvadratom najvećeg linearnog elementa umesto sa fiksnom konstantom, što drži test smislenim bez obzira da li su vaše jedinice tačke ili mikrometri. TryDecompose odustaje na isti način, odbijajući kada dužina prvog reda ili izveden ScaleY padne na ili ispod epsilona

Izaberite stil neuspeha koji odgovara mestu poziva umesto da sve umotavate u try-except iz navike. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds i TryCreateRectMapping vraćaju False i ostavljaju svoje ciljeve netaknutim, što odgovara hit-testiranju i petljama po objektu gde degenerisan objekat treba preskočiti, ne fatalno prekinuti. Invert, InverseCopy, InverseTransformPoint, MapRectToRect i TransformBounds umesto toga bacaju EPdfMatrixError, što odgovara kodu za podešavanje gde singularna matrica znači da je pozivalac nešto pogrešno izračunao. Za batch posao, TransformPoints i TransformRects alociraju svoj rezultujući niz tačno jednom, TransformPointsInPlace i TransformRectsInPlace ponovo koriste vaše skladište, a TryTransformBounds akumulira granični pravougaonik u jednom prolazu umesto da prvo materijalizuje transformisane tačke

Ništa od ovoga nije egzotična matematika. To je jedna konvencija, dosledno primenjena, sa API-jem imenovanim tako da je konvencija vidljiva na mestu poziva: Multiply i obični glagoli dodaju na kraj, porodica Pre dodaje na početak, porodica At uokviruje operaciju svojim parom pivota. Zapišite redosled u komentar pored svakog kompozita koji izgradite, jer kod koji se danas ispravno čita je kod koji će neko obrnuti za šest meseci. Kompletna referenca TPdfMatrix, zajedno sa API-jima page-objekta i renderovanja koje ove transformacije hrane, živi uz PDFium Component za Delphi i C++Builder