PDF afine matrice koriste konvenciju vektora redaka iz ISO 32000-1 §8.3.3, gdje se točka množi s matricom slijeva: point' = point * M. U PDFium Component za Delphi i C++Builder ta jedna činjenica fiksira cijelu API površinu TPdfMatrix: Multiply dodaje na kraj, pa M := M * Op, dok PreMultiply dodaje na početak, pa M := Op * M
Svaki klasičan bug transformacije vraća se na to da je ta rečenica zapamćena unatraške. Vodeni žig koji se uredno rotira u vašoj testnoj datoteci, a slijeće napola izvan stranice u klijentovoj datoteci. Sličica koja izlazi dvostruko rotirana jer je stranica već nosila četvrtinu okreta. Pečat čiji je pomak savršen na A4, a odstupa na Letteru. Nijedan od tih nije bug renderiranja; to su bugovi redoslijeda množenja, i svi su popravljivi čim možete naglas reći u kojem je prostoru svaka operacija napisana
Konvencija vektora redaka koja postavlja pravila
TPdfMatrix pohranjuje šest elemenata imenovanih u specifikaciji i primjenjuje ih točno kako format definira, pa je sama transformacija mjesto gdje rezoniranje počinje. TPdfMatrix.TransformPoint računa 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 definira za operator cm koji konkatenira matricu na trenutnu transformacijsku matricu. Par (a, b) je prvi redak, (c, d) drugi, a (e, f) redak translacije. Navike vektora stupaca pokupljene iz OpenGL-a ili s kolegija linearne algebre ovdje će vas zavesti, i zavest će vas tiho, jer je matrica pogrešnog redoslijeda i dalje savršeno valjana matrica. Čitajte kompozit u konvenciji redaka slijeva nadesno i redoslijed primjene ispadne besplatno: budući da point * (M * Op) jednako (point * M) * Op, dodana operacija na kraju djeluje na koordinate koje je postojeća matrica već proizvela, odnosno na prostor stranice, dok dodana operacija na početku djeluje prije nego postojeća matrica pokrene, u vlastitom 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 prema zadanom rotira u smjeru kazaljke na satu i u stupnjevima, s ACounterClockwise i AAngleInRadians dostupnima kad su vaši izvorni podaci označeni drukčije. Svojstva samo-za-čitanje a do f i svojstvo Handle vraćaju vam sirovi FS_MATRIX natrag, što je ono što FPDFPageObj_SetMatrix želi. Ništa u klasi ne skriva šest brojeva od vas, i to je namjerno: kad se transformacija loše ponaša, ispisivanje a do f najbrža je dijagnoza koju imate
Zašto dodavanje translacije na početak treba linearni dio?
Zato što je dodani pomak na početku napisan u ulaznom prostoru matrice, i mora se provesti kroz trenutni linearni dio prije nego se pridruži retku translacije. TPdfMatrix.PreTranslate stoga računa e := dx*a + dy*c + e i f := dx*b + dy*d + f. Dodavanje na kraj je lakši smjer: TPdfMatrix.Translate napisan je u prostoru stranice, gdje se ništa ne treba pretvarati, pa samo dodaje dx na e i dy na f. Tko god "optimizira" PreTranslate dolje na dva zbrajanja upravo je izbrisao rotaciju i skalu iz pomaka
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 proteže se kroz par skaliranja, i vrijedno je znati koje elemente svaka dotiče prije nego jednu debugirate u tri ujutro. TPdfMatrix.PreScale množi retke, skalirajući a i b s scaleX, a c i d s scaleY, i ostavlja translaciju na miru jer se pomak već dogodio nizvodno. Dodajući na kraj TPdfMatrix.Scale množi stupce umjesto toga, uzimajući a, c, e s scaleX, a b, d, f s scaleY, pa postojeći pomak skalira zajedno sa svime ostalim. Oba su putovi jedne namjene koji preskaču opći produkt od šest elemenata, i oba čuvaju semantiku kompozicije općeg oblika točno
Gdje idu dvije translacije u pivot rotaciji?
Oko operacije, ne oko cijele matrice, i tim redoslijedom. TPdfMatrix.RotateAt dodaje Translate(-pivot), zatim rotaciju, zatim Translate(+pivot), što se pod konvencijom vektora redaka sastavlja kao Translate(-pivot) * Op * Translate(pivot). Taj slijed je ono što drži pivot fiksnim pod novom operacijom dok i dalje dopušta postojećoj matrici da prvo proizvede svoje koordinate, a zatim ih preda dalje. Napišite par obrnutim redoslijedom, kako bi bilo ispravno u biblioteci s vektorima stupaca, i objekt orbitira oko ishodišta umjesto da se okreće na mjestu, što je upravo kako centrirani vodeni žig završi izvan okvira izreza
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 podupire ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt i CentralFlipAt, pa kad jednom vjerujete obrascu za rotaciju, možete mu vjerovati i za ostalo. TPdfMatrix.CentralFlip vrijedi izdvojiti: negira svih šest elemenata da vam dâ okret od 180 stupnjeva bez ikakve trigonometrije, što znači bez cos vrijednosti koja je trebala biti točno nula i bez akumuliranog odstupanja kad ga primijenite u petlji. Ako postavljate ponovljene oznake umjesto okretanja jedne, mehanika samog postavljanja pokrivena je u ponovno upotrebljivim pečatima stranica s Form XObjektima, a matrični posao ovdje sjedi izravno na tome
Što vam TryDecompose govori o matrici?
TPdfMatrix.TryDecompose prijavljuje translaciju, skalu, rotaciju, smicanje, determinantu i zastavicu refleksije pod konvencijom skala-pa-rotacija, i prijavljuje ih dovoljno pošteno da budu korisne za odluke, a ne samo za bilježenje. ScaleX dolazi iz duljine prvog retka, Sqrt(a*a + b*b), pa je uvijek pozitivan. ScaleY je tada Determinant / ScaleX, što ga čini označenim. Rotacija dolazi iz ArcTan2(-b, a) u stupnjevima, a smicanje iz skalarnog umnoška dvaju redaka normaliziranog s obje skale
Taj predznak na ScaleY je dio koji ljudi izbrišu, i brisanje je stvarni bug, a ne kozmetički. Negativna determinanta znači da matrica sadrži refleksiju. Nametnite oba faktora skale pozitivnima kako bi brojevi izgledali urednije, i bacili ste refleksiju, pa se matrica ponovno izgrađena iz dekompozicije vraća zrcaljena: tekst se čita unatrag, skenirana stranica se okrene, uvezeni logotip gleda u pogrešnom smjeru. Polje IsReflected postoji zato da to nikad ne morate zaključivati. Ovo je i provjera koja sprječava klasičnu dvostruku rotaciju, gdje kôd dodaje okret prikaza stranici koja već nosi jedan; verzija tog problema sa strane prikaznika obrađena je u uklapanju sličica, 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 pravokutnika u drugi bez nagađanja
TPdfMatrix.TryCreateRectMapping gradi matricu izvor-u-odredište umjesto vas i uzima TPdfMatrixFitMode od pmfStretch, pmfContain ili pmfCover. Prvo normalizira oba pravokutnika, jer PDF pravokutnici nisu obavezni stići s lijevo ispod desno ili dolje ispod gore, zatim izvodi neovisne X i Y skale: pmfStretch ih drži neovisnima, pmfContain uzima manju i centrira letterbox, pmfCover uzima veću i centrira izrezak. Prateći MapRectToRect dodaje isto mapiranje na kraj postojeće matrice, a NewRectMapping podiže EPdfMatrixError ondje gdje oblik Try vraća False. Ovo je primitiv ispod svakog postavljanja ćelije u N-up montaži i preraspoređivanju stranica, gdje svaka izvorna stranica mora sletjeti unutar izračunate ćelije bez da vi ponovno izvodite aritmetiku po rasporedu
Degenerirane matrice i pošten put neuspjeha
Konačni ulazi ne jamče konačan rezultat, pa kôd uklapanja računa u Double, a zatim ponovno provjerava suženog Single kandidata na konačnost prije objavljivanja; mapiranje koje sadrži beskonačnost nikad se ne vraća kao da je valjano. Ista disciplina upravlja inverzijom. TPdfMatrix.TryGetInverse odbacuje matricu koristeći relativan prag, uspoređujući determinantu s epsilonom pomnoženim kvadratom najvećeg linearnog elementa, a ne s fiksnom konstantom, što je ono što drži test smislenim bez obzira jesu li vaše jedinice točke ili mikrometri. TryDecompose odustaje na isti način, odbijajući kad duljina prvog retka ili izvedeni ScaleY padne na ili ispod epsilona
Odaberite stil neuspjeha koji odgovara mjestu poziva umjesto da sve omotate u try-except iz navike. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds i TryCreateRectMapping vraćaju False i ostavljaju svoje ciljeve netaknutima, što odgovara testiranju pogodaka i petljama po objektu gdje degeneriran objekt treba preskočiti, ne biti fatalan. Invert, InverseCopy, InverseTransformPoint, MapRectToRect i TransformBounds umjesto toga podižu EPdfMatrixError, što odgovara postavnom kôdu gdje singularna matrica znači da je pozivatelj nešto pogrešno izračunao. Za serijski posao, TransformPoints i TransformRects dodjeljuju svoje rezultatsko polje točno jednom, TransformPointsInPlace i TransformRectsInPlace ponovno koriste vašu pohranu, a TryTransformBounds akumulira granični okvir u jednom prolazu umjesto da prvo materijalizira transformirane točke
Ništa od ovoga nije egzotična matematika. Jedna konvencija, dosljedno primijenjena, s API-jem imenovanim tako da je konvencija vidljiva na mjestu poziva: Multiply i obični glagoli dodaju na kraj, obitelj Pre dodaje na početak, obitelj At uokviruje operaciju svojim parom pivota. Zapišite redoslijed u komentar uz svaki kompozit koji izgradite, jer kôd koji se danas ispravno čita netko obrne za šest mjeseci. Potpuna referenca TPdfMatrix, uz API-je objekata stranice i renderiranja koje ove transformacije hrane, živi uz PDFium Component za Delphi i C++Builder