PDF affine матрици използват конвенцията с редови вектори на ISO 32000-1 §8.3.3, където точка се умножава по матрицата отляво: point' = point * M. В PDFium Component за Delphi и C++Builder този факт сам по себе си фиксира цялата API повърхност на TPdfMatrix: Multiply добавя накрая, така че M := M * Op, докато PreMultiply добавя отпред, така че M := Op * M
Всеки класически бъг в трансформации се проследява до това изречение, запомнено наопаки. Водният знак, който се завърта спретнато във вашия тестов файл и кацва наполовина извън страницата в клиентския файл. Миниатюрата, излизаща завъртяна два пъти, защото страницата вече е носила четвърт оборот. Печатът, чийто offset е перфектен на A4 и се измества на Letter. Нищо от това не са бъгове в рендирането; те са бъгове в реда на умножение, и всички са поправими, щом можете да кажете гласно в кое пространство е написана всяка операция
Конвенцията с редови вектори, задаваща правилата
TPdfMatrix съхранява шестте спецификация-именувани елемента и ги прилага точно както форматът ги дефинира, така че самата трансформация е откъдето започва разсъждението. TPdfMatrix.TransformPoint изчислява x' = x*a + y*c + e и y' = x*b + y*d + f, което е шест-елементната форма, дефинирана от ISO 32000-1 §8.3.4 за оператора cm, конкатениращ матрица върху текущата трансформационна матрица. Двойката (a, b) е първият ред, (c, d) вторият, а (e, f) редът за транслация. Навиците за колонни вектори, взети от OpenGL или курс по линейна алгебра, ще ви подведат тук, и ще ви подведат тихо, защото матрица с грешен ред е все още напълно валидна матрица. Прочетете композит в редовата конвенция отляво надясно, и редът на прилагане произтича безплатно: понеже point * (M * Op) е равно на (point * M) * Op, добавена накрая операция действа върху координати, които съществуващата матрица вече е произвела, тоест върху пространството на страницата, докато добавена отпред операция действа преди съществуващата матрица да се изпълни, в собственото входно пространство на обекта
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 по подразбиране е по посока на часовниковата стрелка и в градуси, с ACounterClockwise и AAngleInRadians, налични, когато вашите изходни данни са означени по друг начин. Свойствата само за четене a до f, и свойството Handle ви връщат суровия FS_MATRIX обратно, което е това, което FPDFPageObj_SetMatrix иска. Нищо в класа не крие шестте числа от вас, и това е умишлено: когато трансформация се държи зле, отпечатването на a до f е най-бързата диагноза, която имате
Защо добавянето отпред на транслация се нуждае от линейната част?
Защото изместване, добавено отпред, е написано във входното пространство на матрицата, и трябва да бъде пренесено през текущата линейна част, преди да може да се присъедини към реда за транслация. TPdfMatrix.PreTranslate затова изчислява e := dx*a + dy*c + e и f := dx*b + dy*d + f. Добавянето накрая е лесната посока: TPdfMatrix.Translate е написан в пространството на страницата, където нищо не се нуждае от конвертиране, така че просто добавя dx към e и dy към f. Всеки, който "оптимизира" PreTranslate до две събирания, току-що е изтрил ротацията и мащаба от изместването
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;
Същата асиметрия минава през двойката за мащаб, и си струва да се знае кои елементи докосва всяка, преди да дебъгвате един в три през нощта. TPdfMatrix.PreScale умножава редове, мащабирайки a и b по scaleX, и c и d по scaleY, и оставя транслацията на мира, защото изместването вече се е случило надолу по веригата. Добавящият накрая TPdfMatrix.Scale умножава колони вместо това, взимайки a, c, e по scaleX, и b, d, f по scaleY, така че съществуващият offset се мащабира заедно с всичко останало. И двете са пътища с единична цел, прескачащи общото шест-елементно произведение, и двете запазват точно семантиката на композиция на общата форма
Къде отиват двете транслации в pivot ротация?
Около операцията, не около цялата матрица, и в този ред. TPdfMatrix.RotateAt добавя Translate(-pivot), после ротацията, после Translate(+pivot), което под конвенцията за редови вектори се композира като Translate(-pivot) * Op * Translate(pivot). Тази последователност е това, което държи pivot-а фиксиран под новата операция, докато все още позволява на съществуващата матрица да произведе своите координати първо и да ги предаде нататък. Напишете двойката в обратен ред, както би било правилно в библиотека с колонни вектори, и обектът обикаля около началото, вместо да се върти на място, което е точно как центриран водни знак се озовава извън crop box-а
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;
Същата композиция стои зад ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, и CentralFlipAt, така че щом веднъж повярвате в шаблона за ротация, можете да му вярвате и за останалите. TPdfMatrix.CentralFlip заслужава да се отдели: тя отрича всичките шест елемента, за да ви даде обръщане на 180 градуса без никаква тригонометрия, което означава без cos на стойност, която е трябвало да бъде точно нула, и без натрупващо се отклонение, когато я прилагате в цикъл. Ако поставяте повтарящи се маркировки, вместо да въртите една, механиката на самото поставяне е разгледана в преизползваеми печати на страници с Form XObjects, а работата с матрици тук седи директно върху нея
Какво ви казва TryDecompose за матрица?
TPdfMatrix.TryDecompose отчита транслация, мащаб, ротация, срязване, детерминанта, и флаг за отражение под конвенция мащаб-после-ротация, и ги отчита достатъчно честно, за да е полезно за решения, а не само за логване. ScaleX идва от дължината на първия ред, Sqrt(a*a + b*b), така че винаги е положителен. ScaleY тогава е Determinant / ScaleX, което го прави означен. Ротацията идва от ArcTan2(-b, a) в градуси, а срязването от dot произведението на двата реда, нормализирано по двата мащаба
Този знак на ScaleY е частта, която хората изтриват, а изтриването ѝ е реален бъг, не козметичен. Отрицателна детерминанта означава, че матрицата съдържа отражение. Принудете и двата коефициента за мащаб да са положителни, за да изглеждат числата по-спретнати, и сте изхвърлили отражението, така че матрица, преизградена от декомпозицията, се връща огледално: текстът се чете наопаки, сканирана страница се обръща, импортирано лого гледа в грешна посока. Полето IsReflected съществува, за да не се налага никога да го извеждате. Това е и проверката, предотвратяваща класическото двойно завъртане, при което код добавя display завъртане към страница, вече носеща такова; viewer-side версията на този проблем е разгледана в fit на миниатюра, zoom, и двойно завъртане
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;
Побиране на един правоъгълник в друг без гадаене
TPdfMatrix.TryCreateRectMapping изгражда матрицата от source към destination вместо вас и приема TPdfMatrixFitMode на pmfStretch, pmfContain, или pmfCover. Тя нормализира двата правоъгълника първо, защото PDF правоъгълниците не са задължени да пристигнат с ляво под дясно или долно под горно, после извежда независими X и Y мащаби: pmfStretch ги пази независими, pmfContain взима по-малкия и центрира letterbox-а, pmfCover взима по-големия и центрира изрязването. Съпътстващият MapRectToRect добавя същото картографиране върху съществуваща матрица, а NewRectMapping вдига EPdfMatrixError там, където формата Try връща False. Това е примитивът, стоящ зад всяко поставяне на клетка в N-up imposition и пренареждане на страници, където всяка source страница трябва да кацне вътре в изчислена клетка, без вие да извеждате аритметиката отново по layout
Изродени матрици и честният път за провал
Крайни входове не гарантират краен резултат, така че кодът за побиране изчислява в Double и после повторно проверява стеснения Single кандидат за крайност, преди да го публикува; картографиране, съдържащо безкрайност, никога не се връща сякаш е валидно. Същата дисциплина управлява инверсията. TPdfMatrix.TryGetInverse отхвърля матрица, използвайки относителен праг, сравнявайки детерминантата спрямо епсилон, умножен по квадрата на най-големия линеен елемент, вместо спрямо фиксирана константа, което е това, което пази теста смислен, независимо дали вашите единици са точки или микрометри. TryDecompose се отказва по същия начин, отказвайки, когато дължината на първия ред или изведеният ScaleY падне на или под епсилон
Изберете стила на провал, отговарящ на мястото на извикване, вместо да увивате всичко в try-except по навик. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds, и TryCreateRectMapping връщат False и оставят целите си недокоснати, което подхожда на hit-testing и цикли по обект, където изроден обект трябва да бъде прескочен, не фатален. Invert, InverseCopy, InverseTransformPoint, MapRectToRect, и TransformBounds вдигат EPdfMatrixError вместо това, което подхожда на setup код, където сингулярна матрица означава, че извикващият е изчислил нещо грешно. За batch работа TransformPoints и TransformRects разпределят резултатния си масив точно веднъж, TransformPointsInPlace и TransformRectsInPlace преизползват вашето съхранение, а TryTransformBounds натрупва bounding box-а в едно преминаване, вместо да материализира трансформирани точки първо
Нищо от това не е екзотична математика. Това е една конвенция, приложена последователно, с API, именувано така, че конвенцията да е видима на мястото на извикване: Multiply и обикновените глаголи добавят накрая, семейството Pre добавя отпред, семейството At ограждва операцията с нейния pivot чифт. Запишете реда в коментар до всеки композит, който изградите, защото кодът, четящ се правилно днес, е кодът, който някой обръща след шест месеца. Пълният справочник за TPdfMatrix, заедно с page-object и rendering API-тата, които тези трансформации захранват, живее с PDFium Component за Delphi и C++Builder