PDF-affina matriser använder radvektorkonventionen i ISO 32000-1 §8.3.3, där en punkt multipliceras med matrisen från vänster: point' = point * M. I PDFium Component för Delphi och C++Builder fixerar det faktumet hela API-ytan på TPdfMatrix: Multiply lägger till i slutet, så M := M * Op, medan PreMultiply lägger till i början, så M := Op * M
Varje klassisk transformbugg spårar tillbaka till att den meningen kommits ihåg baklänges. Vattenstämpeln som roterar prydligt i din testfil och hamnar halvt utanför sidan i kundfilen. Miniatyrbilden som kommer ut dubbelroterad eftersom sidan redan bar en kvarts varv. Stämpeln vars förskjutning är perfekt på A4 och driver på Letter. Ingen av dessa är renderingsbuggar; de är multiplikationsordningsbuggar, och de är alla åtgärdbara så snart du kan säga högt vilket rum varje operation är skriven i
Radvektorkonventionen som sätter reglerna
TPdfMatrix lagrar de sex spec-namngivna elementen och tillämpar dem exakt som formatet definierar dem, så själva transformen är där resonemanget börjar. TPdfMatrix.TransformPoint beräknar x' = x*a + y*c + e och y' = x*b + y*d + f, vilket är den sexelementsform ISO 32000-1 §8.3.4 definierar för cm-operatorn som sammansätter en matris på den aktuella transformationsmatrisen. Paret (a, b) är den första raden, (c, d) den andra, och (e, f) translationsraden. Kolumnvektorvanor plockade upp från OpenGL eller från en linjär algebra-kurs kommer att vilseleda dig här, och de kommer att vilseleda dig tyst, eftersom en matris i fel ordning fortfarande är en fullt giltig matris. Läs en sammansättning i radkonventionen från vänster till höger och ordningen på tillämpningen faller ut gratis: eftersom point * (M * Op) är lika med (point * M) * Op, verkar en tillagd operation på koordinater den befintliga matrisen redan har producerat, det vill säga i sidrummet, medan en operation lagd till i början verkar innan den befintliga matrisen körs, i objektets eget indatarum
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 är som standard medurs och i grader, med ACounterClockwise och AAngleInRadians tillgängliga när din källdata är tecknad åt andra hållet. De skrivskyddade egenskaperna a till f och egenskapen Handle ger dig tillbaka de råa FS_MATRIX, vilket är vad FPDFPageObj_SetMatrix vill ha. Ingenting i klassen döljer de sex talen för dig, och det är avsiktligt: när en transform beter sig illa är det snabbaste sättet att diagnostisera att skriva ut a till f
Varför behöver en tillagd-i-början translation den linjära delen?
Därför att en förskjutning lagd till i början är skriven i matrisens indatarum, och den måste bäras genom den aktuella linjära delen innan den kan förena sig med translationsraden. TPdfMatrix.PreTranslate beräknar därför e := dx*a + dy*c + e och f := dx*b + dy*d + f. Att lägga till i slutet är den enkla riktningen: TPdfMatrix.Translate är skriven i sidrummet, där ingenting behöver konverteras, så den lägger bara till dx till e och dy till f. Den som "optimerar" PreTranslate ner till två additioner har just raderat rotationen och skalan från förskjutningen
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;
Samma asymmetri löper genom skalparet, och det är värt att veta vilka element var och en rör vid innan du felsöker en klockan tre på morgonen. TPdfMatrix.PreScale multiplicerar rader, skalar a och b med scaleX och c och d med scaleY, och den lämnar translationen orörd eftersom förskjutningen redan skedde nedströms. Den tillagda-i-slutet TPdfMatrix.Scale multiplicerar kolumner istället, tar a, c, e med scaleX och b, d, f med scaleY, så den befintliga offseten skalas med allt annat. Båda är enkelsyftade vägar som hoppar över den generella sexelementsprodukten, och båda bevarar sammansättningssemantiken hos den generella formen exakt
Var hamnar de två translationerna i en pivotrotation?
Runt operationen, inte runt hela matrisen, och i den ordningen. TPdfMatrix.RotateAt lägger till Translate(-pivot), sedan rotationen, sedan Translate(+pivot), vilket under radvektorkonventionen sätts samman som Translate(-pivot) * Op * Translate(pivot). Den sekvensen är det som håller pivoten fast under den nya operationen medan den befintliga matrisen ändå producerar sina koordinater först och lämnar dem vidare. Skriv paret åt andra hållet, som skulle vara korrekt i ett kolumnvektorbibliotek, och objektet kretsar runt origo istället för att snurra på plats, vilket är precis hur en centrerad vattenstämpel hamnar utanför beskärningsrutan
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;
Samma sammansättning ligger bakom ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt och CentralFlipAt, så när du väl litar på mönstret för rotation kan du lita på det för resten. TPdfMatrix.CentralFlip är värd att lyfta fram särskilt: den negerar alla sex element för att ge dig en 180-graders vändning utan någon trigonometri alls, vilket betyder ingen cos av ett värde som borde varit exakt noll och ingen ackumulerande drift när du tillämpar den i en loop. Om du placerar upprepade märken snarare än att vända ett, täcks mekaniken bakom själva placeringen i återanvändbara sidstämplar med formulär-XObjekt, och matrisarbetet här sitter direkt ovanpå det
Vad berättar TryDecompose om en matris?
TPdfMatrix.TryDecompose rapporterar translation, skala, rotation, skjuvning, determinant och en reflektionsflagga under en skala-sedan-rotation-konvention, och den rapporterar dem ärligt nog för att vara användbara för beslut snarare än bara för loggning. ScaleX kommer från längden på den första raden, Sqrt(a*a + b*b), så den är alltid positiv. ScaleY är sedan Determinant / ScaleX, vilket gör den tecknad. Rotation kommer från ArcTan2(-b, a) i grader, och skjuvning från de två radernas skalärprodukt normaliserad med båda skalorna
Det tecknet på ScaleY är delen folk raderar, och att radera den är en verklig bugg snarare än en kosmetisk. En negativ determinant betyder att matrisen innehåller en reflektion. Tvinga båda skalfaktorerna positiva för att få talen att se snyggare ut och du har kastat bort reflektionen, så en matris återuppbyggd från nedbrytningen kommer tillbaka speglad: text läses baklänges, en skannad sida vänds, en importerad logotyp vänder åt fel håll. Fältet IsReflected finns så du aldrig behöver gissa dig till det. Det här är också kontrollen som hindrar den klassiska dubbelrotationen, där kod lägger till en visningsvridning till en sida som redan bär en; visningssidans version av det problemet arbetas igenom i miniatyrbildsanpassning, zoom och dubbelrotation
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;
Att passa in en rektangel i en annan utan att gissa
TPdfMatrix.TryCreateRectMapping bygger käll-till-mål-matrisen åt dig och tar ett TPdfMatrixFitMode av pmfStretch, pmfContain, eller pmfCover. Den normaliserar båda rektanglarna först, eftersom PDF-rektanglar inte behöver anlända med vänster under höger eller nedtill under upptill, och härleder sedan oberoende X- och Y-skalor: pmfStretch håller dem oberoende, pmfContain tar den mindre och centrerar brevlådeformatet, pmfCover tar den större och centrerar beskärningen. Följeslagaren MapRectToRect lägger till samma mappning på en befintlig matris, och NewRectMapping höjer EPdfMatrixError där Try-formen returnerar False. Det här är primitiven bakom varje cellplacering i N-up-utlägg och sidomordning, där varje källsida måste landa inuti en beräknad cell utan att du behöver härleda aritmetiken igen per layout
Degenererade matriser och den ärliga felvägen
Ändliga indata garanterar inte ett ändligt resultat, så anpassningskoden beräknar i Double och kontrollerar sedan om den nedskalade Single-kandidaten är ändlig innan den publiceras; en mappning som innehåller en oändlighet lämnas aldrig tillbaka som om den vore giltig. Samma disciplin styr invertering. TPdfMatrix.TryGetInverse avvisar en matris med hjälp av ett relativt tröskelvärde, jämför determinanten mot epsilon gånger kvadraten på det största linjära elementet snarare än mot en fast konstant, vilket är det som håller testet meningsfullt oavsett om dina enheter är punkter eller mikrometer. TryDecompose hoppar av på samma sätt, vägrar när den första radens längd eller den härledda ScaleY faller på eller under epsilon
Välj felstilen som passar anropsstället snarare än att slå in allt i try-except av vana. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds och TryCreateRectMapping returnerar False och lämnar sina mål orörda, vilket passar hit-testning och per-objekt-loopar där ett degenererat objekt bör hoppas över, inte vara ödesdigert. Invert, InverseCopy, InverseTransformPoint, MapRectToRect och TransformBounds höjer EPdfMatrixError istället, vilket passar installationskod där en singulär matris betyder att anroparen beräknade något fel. För batcharbete allokerar TransformPoints och TransformRects sin resultatarray exakt en gång, TransformPointsInPlace och TransformRectsInPlace återanvänder ditt lagringsutrymme, och TryTransformBounds ackumulerar begränsningsrutan i en enda genomgång istället för att materialisera transformerade punkter först
Inget av detta är exotisk matematik. Det är en konvention, konsekvent tillämpad, med API:et namngivet så konventionen är synlig vid anropsstället: Multiply och de vanliga verben lägger till i slutet, Pre-familjen lägger till i början, At-familjen bygger in operationen mellan dess pivotpar. Skriv ner ordningen i en kommentar bredvid varje sammansättning du bygger, för koden som läses korrekt idag är koden någon vänder på om sex månader. Den fullständiga TPdfMatrix-referensen, tillsammans med sidobjekts- och rendering-API:erna dessa transformer matar in i, finns hos PDFium Component för Delphi och C++Builder