Macierze afiniczne PDF używają konwencji wektora wierszowego z ISO 32000-1 §8.3.3, w której punkt mnoży macierz od lewej: point' = point * M. W PDFium Component dla Delphi i C++Buildera ten jeden fakt ustala całą powierzchnię API TPdfMatrix: Multiply dołącza, więc M := M * Op, podczas gdy PreMultiply poprzedza, więc M := Op * M
Każdy klasyczny błąd transformacji sprowadza się do zapamiętania tego zdania na odwrót. Znak wodny, który obraca się gładko w twoim pliku testowym, a w pliku klienta ląduje w połowie poza stroną. Miniatura wychodząca obrócona dwukrotnie, bo strona już niosła ćwierć obrotu. Pieczątka, której przesunięcie jest idealne na A4, a dryfuje na Letter. Żaden z tych przypadków nie jest błędem renderowania; to błędy kolejności mnożenia, i wszystkie da się naprawić, gdy tylko potrafisz na głos powiedzieć, w jakiej przestrzeni zapisana jest każda operacja
Konwencja wektora wierszowego ustalająca reguły
TPdfMatrix przechowuje sześć elementów nazwanych przez specyfikację i stosuje je dokładnie tak, jak definiuje je format, więc od samej transformacji zaczyna się rozumowanie. TPdfMatrix.TransformPoint oblicza x' = x*a + y*c + e oraz y' = x*b + y*d + f, co jest sześcioelementową postacią, którą ISO 32000-1 §8.3.4 definiuje dla operatora cm, konkatenującego macierz na bieżącą macierz transformacji. Para (a, b) to pierwszy wiersz, (c, d) to drugi, a (e, f) to wiersz przesunięcia. Nawyki wektora kolumnowego wyniesione z OpenGL lub z kursu algebry liniowej wprowadzą cię tu w błąd, i zrobią to po cichu, ponieważ macierz w złej kolejności jest wciąż macierzą całkowicie poprawną. Czytaj złożenie w konwencji wierszowej od lewej do prawej, a kolejność stosowania wypada za darmo: skoro point * (M * Op) równa się (point * M) * Op, dołączona operacja działa na współrzędnych, które istniejąca macierz już wyprodukowała, czyli na przestrzeni strony, podczas gdy poprzedzona operacja działa przed uruchomieniem istniejącej macierzy, we własnej przestrzeni wejściowej obiektu
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 domyślnie obraca zgodnie z ruchem wskazówek zegara i w stopniach, z ACounterClockwise i AAngleInRadians dostępnymi, gdy twoje dane źródłowe są oznaczone odwrotnie. Właściwości tylko do odczytu a do f oraz właściwość Handle dają ci z powrotem surowy FS_MATRIX, czego chce FPDFPageObj_SetMatrix. Nic w tej klasie nie ukrywa przed tobą sześciu liczb, i to jest celowe: gdy transformacja się psuje, wydrukowanie a do f to najszybsza diagnoza, jaką masz
Dlaczego poprzedzenie przesunięciem potrzebuje części liniowej?
Ponieważ poprzedzone przesunięcie jest zapisane w przestrzeni wejściowej macierzy i musi zostać przeprowadzone przez bieżącą część liniową, zanim dołączy do wiersza przesunięcia. TPdfMatrix.PreTranslate oblicza więc e := dx*a + dy*c + e oraz f := dx*b + dy*d + f. Dołączanie to łatwiejszy kierunek: TPdfMatrix.Translate jest zapisane w przestrzeni strony, gdzie nic nie wymaga konwersji, więc po prostu dodaje dx do e i dy do f. Ktokolwiek „zoptymalizuje" PreTranslate do dwóch dodawań, właśnie usunął z przesunięcia rotację i skalę
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;
Ta sama asymetria przebiega przez parę skalowania, i warto wiedzieć, które elementy dotyka każda z nich, zanim zaczniesz debugować jedną o trzeciej w nocy. TPdfMatrix.PreScale mnoży wiersze, skalując a i b przez scaleX, a c i d przez scaleY, i zostawia przesunięcie bez zmian, ponieważ przesunięcie już się wydarzyło dalej w łańcuchu. Dołączające TPdfMatrix.Scale mnoży kolumny zamiast tego, biorąc a, c, e przez scaleX i b, d, f przez scaleY, więc istniejące przesunięcie skaluje się razem ze wszystkim innym. Obie to jednocelowe ścieżki pomijające ogólny sześcioelementowy iloczyn, i obie zachowują semantykę złożenia postaci ogólnej dokładnie
Gdzie w rotacji wokół osi trafiają dwa przesunięcia?
Wokół operacji, nie wokół całej macierzy, i w tej właśnie kolejności. TPdfMatrix.RotateAt dołącza Translate(-pivot), potem rotację, potem Translate(+pivot), co pod konwencją wektora wierszowego składa się jako Translate(-pivot) * Op * Translate(pivot). Ta sekwencja jest tym, co utrzymuje oś nieruchomą pod nową operacją, wciąż pozwalając istniejącej macierzy najpierw wyprodukować swoje współrzędne i przekazać je dalej. Zapisz tę parę w odwrotnej kolejności, tak jak byłaby poprawna w bibliotece wektora kolumnowego, a obiekt zacznie krążyć wokół początku układu zamiast obracać się w miejscu, co dokładnie tłumaczy, jak wyśrodkowany znak wodny kończy poza obszarem przycinania
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;
To samo złożenie stoi za ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt i CentralFlipAt, więc gdy raz zaufasz temu wzorcowi dla rotacji, możesz zaufać mu też dla reszty. TPdfMatrix.CentralFlip zasługuje na wyróżnienie: neguje wszystkie sześć elementów, dając obrót o 180 stopni bez żadnej trygonometrii, co oznacza brak cos wartości, która powinna była wyjść dokładnie zerem, i brak narastającego dryfu, gdy stosujesz to w pętli. Jeśli umieszczasz powtarzane znaczniki zamiast obracać jeden, mechanika samego umieszczania jest omówiona w wielokrotnych pieczątkach strony z Form XObjects, a praca z macierzami tutaj siedzi bezpośrednio na niej
Co TryDecompose mówi ci o macierzy?
TPdfMatrix.TryDecompose zgłasza przesunięcie, skalę, rotację, ścinanie, wyznacznik i flagę odbicia w konwencji skala-a-potem-rotacja, i zgłasza je na tyle uczciwie, by były użyteczne do decyzji, a nie tylko do logowania. ScaleX pochodzi z długości pierwszego wiersza, Sqrt(a*a + b*b), więc jest zawsze dodatnia. ScaleY to wtedy Determinant / ScaleX, co czyni ją ze znakiem. Rotacja pochodzi z ArcTan2(-b, a) w stopniach, a ścinanie z iloczynu skalarnego obu wierszy znormalizowanego przez obie skale
Ten znak przy ScaleY to część, którą ludzie usuwają, a usunięcie jej to prawdziwy błąd, nie kosmetyczny. Ujemny wyznacznik oznacza, że macierz zawiera odbicie. Wymuś, by oba czynniki skali były dodatnie, aby liczby wyglądały schludniej, a wyrzuciłeś odbicie: macierz odtworzona z rozkładu wraca odbita lustrzanie — tekst czyta się od tyłu, zeskanowana strona się odwraca, zaimportowane logo patrzy w złą stronę. Pole IsReflected istnieje właśnie po to, byś nigdy nie musiał tego zgadywać. To także sprawdzenie zapobiegające klasycznej podwójnej rotacji, gdzie kod dodaje obrót wyświetlania do strony, która już niesie własny; wersja tego problemu po stronie przeglądarki jest przepracowana w dopasowaniu miniatury, powiększeniu i podwójnej rotacji
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;
Dopasowanie jednego prostokąta do drugiego bez zgadywania
TPdfMatrix.TryCreateRectMapping buduje za ciebie macierz źródło-do-celu i przyjmuje TPdfMatrixFitMode równy pmfStretch, pmfContain lub pmfCover. Najpierw normalizuje oba prostokąty, ponieważ prostokąty PDF nie muszą przybywać z lewym poniżej prawego ani dolnym poniżej górnego, a potem wyprowadza niezależne skale X i Y: pmfStretch zachowuje je niezależne, pmfContain bierze mniejszą z nich i centruje letterbox, pmfCover bierze większą z nich i centruje kadrowanie. Towarzyszące MapRectToRect dołącza to samo mapowanie do istniejącej macierzy, a NewRectMapping rzuca EPdfMatrixError tam, gdzie forma Try zwraca False. To prymityw stojący pod każdym umieszczeniem komórki w imposition N-up i zmianie kolejności stron, gdzie każda strona źródłowa musi wylądować w obliczonej komórce bez ponownego wyprowadzania arytmetyki na każdy układ
Macierze zdegenerowane i uczciwa ścieżka awarii
Skończone wejścia nie gwarantują skończonego wyniku, więc kod dopasowania oblicza w Double, a następnie ponownie sprawdza zawężonego kandydata Single pod kątem skończoności, zanim go opublikuje; mapowanie zawierające nieskończoność nigdy nie jest zwracane tak, jakby było poprawne. Ta sama dyscyplina rządzi odwracaniem. TPdfMatrix.TryGetInverse odrzuca macierz progiem względnym, porównując wyznacznik z epsilonem razy kwadrat największego elementu liniowego zamiast ze stałą wartością, co utrzymuje sensowność testu niezależnie od tego, czy twoje jednostki to punkty czy mikrometry. TryDecompose również się wycofuje w ten sposób, odmawiając, gdy długość pierwszego wiersza lub wyprowadzona ScaleY spadnie do lub poniżej epsilona
Wybierz styl awarii pasujący do miejsca wywołania, zamiast owijać wszystko w try-except z przyzwyczajenia. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds i TryCreateRectMapping zwracają False i zostawiają swoje cele nietknięte, co pasuje do testowania trafień i pętli per obiekt, gdzie zdegenerowany obiekt powinien być pominięty, nie fatalny. Invert, InverseCopy, InverseTransformPoint, MapRectToRect i TransformBounds rzucają zamiast tego EPdfMatrixError, co pasuje do kodu konfiguracyjnego, gdzie osobliwa macierz oznacza, że wywołujący coś źle obliczył. Do pracy wsadowej TransformPoints i TransformRects alokują swoją tablicę wynikową dokładnie raz, TransformPointsInPlace i TransformRectsInPlace ponownie wykorzystują twoją pamięć, a TryTransformBounds akumuluje ramkę ograniczającą w jednym przebiegu zamiast najpierw materializować przekształcone punkty
Nic z tego nie jest egzotyczną matematyką. To jedna konwencja, stosowana konsekwentnie, z API nazwanym tak, by konwencja była widoczna w miejscu wywołania: Multiply i zwykłe czasowniki dołączają, rodzina Pre poprzedza, rodzina At obudowuje operację jej parą pivotu. Zapisz kolejność w komentarzu obok każdego złożenia, które budujesz, bo kod, który dziś czyta się poprawnie, to kod, który ktoś odwróci za sześć miesięcy. Pełne odniesienie TPdfMatrix, wraz z API obiektów strony i renderowania, które te transformacje zasilają, żyje razem z PDFium Component dla Delphi i C++Buildera