PDF-affine matricer bruger rækkevektor-konventionen fra ISO 32000-1 §8.3.3, hvor et punkt ganger matricen fra venstre: point' = point * M. I PDFium Component til Delphi og C++Builder fastsætter det ene faktum hele API-fladen af TPdfMatrix: Multiply appender, så M := M * Op, mens PreMultiply prepender, så M := Op * M
Hver klassisk transform-fejl spores tilbage til, at den sætning bliver husket baglæns. Vandmærket, der roterer pænt i din testfil og lander halvt uden for siden i kundefilen. Miniaturebilledet, der kommer ud roteret to gange, fordi siden allerede bar en kvart omdrejning. Stemplet, hvis offset er perfekt på A4 og driver på Letter. Ingen af dem er rendering-fejl; de er multiplikationsrækkefølge-fejl, og de er alle rettelige, når man kan sige højt hvilket rum hver operation er skrevet i
Rækkevektor-konventionen der sætter reglerne
TPdfMatrix gemmer de seks spec-navngivne elementer og anvender dem præcis som formatet definerer dem, så selve transformen er der, hvor ræsonnementet begynder. TPdfMatrix.TransformPoint beregner x' = x*a + y*c + e og y' = x*b + y*d + f, hvilket er den seks-element-form ISO 32000-1 §8.3.4 definerer for cm-operatoren, der sammenkæder en matrix på den aktuelle transformationsmatrix. Parret (a, b) er den første række, (c, d) den anden, og (e, f) oversættelsesrækken. Kolonnevektor-vaner samlet op fra OpenGL eller fra et lineær algebra-kursus vil vildlede dig her, og de vil vildlede dig stille, fordi en forkert-rækkefølge-matrix stadig er en helt gyldig matrix. Læs en sammensætning i rækkekonventionen fra venstre til højre, og anvendelsesrækkefølgen falder ud gratis: siden point * (M * Op) svarer til (point * M) * Op, virker en appended operation på koordinater, den eksisterende matrix allerede har produceret, altså på siderum, mens en prepended operation virker før den eksisterende matrix kører, i objektets eget inputrum
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 er som standard med uret og i grader, med ACounterClockwise og AAngleInRadians tilgængelige, når ens kildedata er signeret den anden vej. De skrivebeskyttede a-til-f-egenskaber og Handle-egenskaben giver dig den rå FS_MATRIX tilbage, hvilket er hvad FPDFPageObj_SetMatrix vil have. Intet i klassen skjuler de seks tal for dig, og det er bevidst: når en transform opfører sig dårligt, er at printe a til f den hurtigste diagnose, man har
Hvorfor har en prepended oversættelse brug for den lineære del?
Fordi et prepended skift er skrevet i matricens inputrum, og det skal føres gennem den aktuelle lineære del, før det kan slutte sig til oversættelsesrækken. TPdfMatrix.PreTranslate beregner derfor e := dx*a + dy*c + e og f := dx*b + dy*d + f. At appende er den lette retning: TPdfMatrix.Translate er skrevet i siderum, hvor intet skal konverteres, så den lægger blot dx til e og dy til f. Enhver, der "optimerer" PreTranslate ned til to additioner, har lige slettet rotationen og skalaen fra skiftet
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;
Den samme asymmetri løber gennem skala-parret, og det er værd at vide, hvilke elementer hver enkelt rører, før man debugger en klokken tre om natten. TPdfMatrix.PreScale ganger rækker, skalerer a og b med scaleX og c og d med scaleY, og den lader oversættelsen være i fred, fordi skiftet allerede skete nedstrøms. Den appenderende TPdfMatrix.Scale ganger kolonner i stedet, og tager a, c, e med scaleX og b, d, f med scaleY, så den eksisterende offset skalerer med alt andet. Begge er enkeltformåls-stier, der springer over det generelle seks-element-produkt, og begge bevarer sammensætningssemantikken af den generelle form præcist
Hvor går de to oversættelser i en pivot-rotation?
Omkring operationen, ikke omkring hele matricen, og i den rækkefølge. TPdfMatrix.RotateAt appender Translate(-pivot), så rotationen, så Translate(+pivot), hvilket under rækkevektor-konventionen sammensættes som Translate(-pivot) * Op * Translate(pivot). Den sekvens er hvad der holder pivoten fast under den nye operation, mens den stadig lader den eksisterende matrix producere sine koordinater først og overgive dem videre. Skriv parret den anden vej rundt, som det ville være korrekt i et kolonnevektor-bibliotek, og objektet kredser om origo i stedet for at spinde på stedet, hvilket er præcis hvordan et centreret vandmærke ender uden for beskæringsboksen
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;
Den samme sammensætning bakker ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt og CentralFlipAt op, så når man først stoler på mønstret for rotation, kan man stole på det for resten. TPdfMatrix.CentralFlip er værd at fremhæve særskilt: den negerer alle seks elementer for at give en 180-graders vending uden nogen trigonometri overhovedet, hvilket betyder ingen cos af en værdi, der burde have været præcis nul, og ingen akkumulerende drift, når man anvender den i en løkke. Hvis man placerer gentagne mærker frem for at vende ét, dækkes mekanikken bag selve placeringen i genbrugelige sidestempler med Form XObjects, og matrixarbejdet her sidder direkte oven på det
Hvad fortæller TryDecompose dig om en matrix?
TPdfMatrix.TryDecompose rapporterer oversættelse, skala, rotation, forskydning, determinant og et refleksionsflag under en skala-så-rotation-konvention, og den rapporterer dem ærligt nok til at være nyttige for beslutninger frem for blot til logning. ScaleX kommer fra længden af den første række, Sqrt(a*a + b*b), så den er altid positiv. ScaleY er så Determinant / ScaleX, hvilket gør den signeret. Rotation kommer fra ArcTan2(-b, a) i grader, og forskydning fra de to rækkers prikprodukt normaliseret med begge skalaer
Det fortegn på ScaleY er den del, folk sletter, og at slette det er en reel fejl frem for en kosmetisk en. En negativ determinant betyder, at matricen indeholder en refleksion. Tving begge skalafaktorer positive for at få tallene til at se pænere ud, og man har kastet refleksionen væk, så en matrix genopbygget fra dekompositionen kommer tilbage spejlvendt: tekst læses baglæns, en scannet side vender om, et importeret logo vender den forkerte vej. IsReflected-feltet findes, så man aldrig behøver udlede det. Dette er også tjekket, der forhindrer den klassiske dobbeltrotation, hvor kode tilføjer en visningsvending til en side, der allerede bærer en; viewer-side-versionen af det problem gennemgås i miniature-tilpasning, zoom og dobbeltrotation
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;
At tilpasse ét rektangel ind i et andet uden at gætte
TPdfMatrix.TryCreateRectMapping bygger kilde-til-destination-matricen for dig og tager en TPdfMatrixFitMode af pmfStretch, pmfContain eller pmfCover. Den normaliserer begge rektangler først, fordi PDF-rektangler ikke er krævet at ankomme med venstre under højre eller bund under top, og udleder så uafhængige X- og Y-skalaer: pmfStretch holder dem uafhængige, pmfContain tager den mindre og centrerer letterboxen, pmfCover tager den større og centrerer beskæringen. Følgeskabs-MapRectToRect appender den samme mapning på en eksisterende matrix, og NewRectMapping rejser EPdfMatrixError, hvor Try-formen returnerer False. Dette er primitiven under hver celleplacering i N-up-imposition og sideomordning, hvor hver kildeside skal lande inde i en beregnet celle uden at man genudleder aritmetikken per layout
Degenererede matricer og den ærlige fejlsti
Endelige input garanterer ikke et endeligt resultat, så tilpasningskoden beregner i Double og tjekker så den indsnævrede Single-kandidat igen for endelighed, før den udgives; en mapning, der indeholder en uendelighed, gives aldrig tilbage, som var den gyldig. Den samme disciplin styrer inversion. TPdfMatrix.TryGetInverse afviser en matrix ved brug af en relativ tærskel, der sammenligner determinanten mod epsilon gange kvadratet af det største lineære element frem for mod en fast konstant, hvilket er hvad der holder testen meningsfuld, uanset om ens enheder er punkter eller mikrometer. TryDecompose bakker ud på samme måde og nægter, når den første rækkes længde eller den udledte ScaleY falder ved eller under epsilon
Vælg den fejlstil, der passer til kaldestedet, frem for at pakke alt ind i try-except af vane. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds og TryCreateRectMapping returnerer False og lader deres mål være urørte, hvilket passer til hit-testing og per-objekt-løkker, hvor et degenereret objekt bør springes over, ikke være fatalt. Invert, InverseCopy, InverseTransformPoint, MapRectToRect og TransformBounds rejser i stedet EPdfMatrixError, hvilket passer til opsætningskode, hvor en singulær matrix betyder, at kalderen beregnede noget forkert. For batch-arbejde allokerer TransformPoints og TransformRects deres resultatarray præcis én gang, TransformPointsInPlace og TransformRectsInPlace genbruger ens lagring, og TryTransformBounds akkumulerer den omsluttende boks i én gennemgang frem for at materialisere transformerede punkter først
Intet af dette er eksotisk matematik. Det er én konvention, anvendt konsistent, med API'en navngivet så konventionen er synlig ved kaldestedet: Multiply og de rene verber appender, Pre-familien prepender, At-familien parentetiserer operationen med dens pivot-par. Skriv rækkefølgen ned i en kommentar ved siden af enhver sammensætning, man bygger, fordi koden der læser korrekt i dag, er koden nogen vender om om seks måneder. Den fulde TPdfMatrix-reference, sammen med side-objekt- og rendering-API'erne disse transformer fodrer ind i, bor hos PDFium Component til Delphi og C++Builder