Афінні матриці PDF використовують конвенцію рядкових векторів з ISO 32000-1 §8.3.3, де точка множить матрицю зліва: point' = point * M. У PDFium Component для Delphi та C++Builder цей один факт фіксує всю поверхню API TPdfMatrix: Multiply додає в кінець, тож M := M * Op, тоді як PreMultiply додає на початок, тож M := Op * M
Кожен класичний баг перетворень веде до того, що це речення запам'ятали задом наперед. Водяний знак, що акуратно обертається у твоєму тестовому файлі й приземляється наполовину поза сторінкою в файлі клієнта. Мініатюра, що виходить обернутою двічі, бо сторінка вже несла чверть повороту. Штамп, чий зсув ідеальний на 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, тож наявний зсув масштабується разом з усім іншим. Обидва — шляхи з однією ціллю, що пропускають загальний шестиелементний добуток, і обидва зберігають семантику композиції загальної форми рівно так само
Куди йдуть два перенесення при обертанні навколо точки опори?
Навколо операції, а не навколо всієї матриці, і в такому порядку. TPdfMatrix.RotateAt додає в кінець Translate(-pivot), потім обертання, потім Translate(+pivot), що за конвенцією рядкових векторів компонується як Translate(-pivot) * Op * Translate(pivot). Ця послідовність — те, що тримає точку опори фіксованою під новою операцією, дозволяючи водночас наявній матриці спершу виробити свої координати й передати їх далі. Напиши цю пару в зворотному порядку, як було б правильно в бібліотеці стовпцевих векторів, і об'єкт обертається навколо початку координат замість того, щоб крутитися на місці, що рівно так центрований водяний знак опиняється поза рамкою обрізки
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) у градусах, а зсув — з добутку двох рядків, нормалізованого обома масштабами
Цей знак на ScaleY — та частина, яку люди видаляють, а видалення його — справжній баг, а не косметичний. Від'ємний детермінант означає, що матриця містить відображення. Примусово зроби обидва масштабні коефіцієнти додатними, щоб числа виглядали охайніше, і ти викинув відображення, тож матриця, перебудована з розкладу, повертається дзеркальною: текст читається задом наперед, скановна сторінка перевертається, імпортований логотип дивиться не в той бік. Поле IsReflected існує, щоб тобі ніколи не довелося це виводити. Це також перевірка, що запобігає класичному подвійному обертанню, коли код додає поворот дисплея до сторінки, що вже несе один; версія цієї проблеми на боці переглядача розглянута в припасовуванні мініатюр, масштабуванні та подвійному обертанні
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 будує матрицю джерело-в-ціль за тебе й приймає TPdfMatrixFitMode зі значень pmfStretch, pmfContain чи pmfCover. Він спершу нормалізує обидва прямокутники, бо прямокутники PDF не зобов'язані прибувати з лівим краєм нижче правого чи нижнім нижче верхнього, потім виводить незалежні масштаби X та Y: pmfStretch тримає їх незалежними, pmfContain бере менший і центрує лейтербокс, pmfCover бере більший і центрує обрізку. Супровідний MapRectToRect додає в кінець те саме відображення на наявну матрицю, а NewRectMapping піднімає EPdfMatrixError там, де форма Try повертає False. Це примітив, що лежить під кожним розміщенням комірки в N-up спуску смуг та переупорядкуванні сторінок, де кожна вихідна сторінка мусить приземлитися всередині обчисленої комірки без повторного виведення арифметики на розкладку
Вироджені матриці й чесний шлях відмови
Скінченні входи не гарантують скінченного результату, тож код припасування обчислює в Double, а потім повторно перевіряє звужений кандидат Single на скінченність, перш ніж публікувати його; відображення, що містить нескінченність, ніколи не повертається назад так, ніби воно валідне. Та сама дисципліна керує інверсією. TPdfMatrix.TryGetInverse відхиляє матрицю, використовуючи відносний поріг, порівнюючи детермінант з епсилон, помноженим на квадрат найбільшого лінійного елемента, а не з фіксованою константою, що і тримає перевірку осмисленою незалежно від того, чи твої одиниці — точки чи мікрометри. TryDecompose відступає так само, відмовляючи, коли довжина першого рядка чи виведений ScaleY опиняється на чи нижче епсилон
Обирай стиль відмови, що відповідає точці виклику, а не обгортай усе в try-except за звичкою. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds та TryCreateRectMapping повертають False і лишають свої цілі недоторканими, що підходить для тестування влучання й циклів по об'єктах, де вироджений об'єкт слід пропустити, а не зробити фатальним. Invert, InverseCopy, InverseTransformPoint, MapRectToRect та TransformBounds натомість піднімають EPdfMatrixError, що підходить для коду налаштування, де сингулярна матриця означає, що викликач обчислив щось неправильно. Для пакетної роботи TransformPoints та TransformRects виділяють свій масив результату рівно один раз, TransformPointsInPlace та TransformRectsInPlace повторно використовують твоє сховище, а TryTransformBounds накопичує обмежувальний прямокутник за один прохід замість матеріалізації перетворених точок спершу
Ніщо з цього — не екзотична математика. Це одна конвенція, застосована послідовно, з API, названим так, щоб конвенція була видима в точці виклику: Multiply та прості дієслова додають у кінець, родина Pre додає на початок, родина At бере операцію в дужки з її парою точки опори. Запиши порядок у коментарі поруч з будь-яким композитом, який будуєш, бо код, що читається правильно сьогодні, — це код, який хтось розвертає через шість місяців. Повний довідник TPdfMatrix, разом з API об'єктів сторінки й рендерингу, які живлять ці перетворення, живе разом з PDFium Component для Delphi та C++Builder