Ma trận affine của PDF dùng quy ước vector hàng theo ISO 32000-1 §8.3.3, nơi một điểm nhân với ma trận từ bên trái: point' = point * M. Trong PDFium Component cho Delphi và C++Builder, chính sự thật đó cố định toàn bộ bề mặt API của TPdfMatrix: Multiply nối thêm, nên M := M * Op, trong khi PreMultiply chèn trước, nên M := Op * M
Mọi lỗi biến đổi kinh điển đều bắt nguồn từ việc câu đó bị nhớ ngược. Watermark xoay gọn gàng trong tệp test của bạn nhưng lại rơi ra ngoài trang một nửa trong tệp của khách hàng. Thumbnail ra kết quả bị xoay hai lần vì trang đã mang sẵn một phần tư vòng quay. Con dấu có offset hoàn hảo trên A4 nhưng trôi dạt trên Letter. Không cái nào trong số đó là lỗi render; chúng là lỗi thứ tự nhân, và tất cả đều có thể sửa được một khi bạn có thể nói rõ ràng mỗi thao tác được viết trong không gian nào
Quy ước vector hàng thiết lập các quy tắc
TPdfMatrix lưu sáu phần tử được đặc tả đặt tên và áp dụng chúng đúng như định dạng định nghĩa, nên chính phép biến đổi là nơi lập luận bắt đầu. TPdfMatrix.TransformPoint tính x' = x*a + y*c + e và y' = x*b + y*d + f, đây là dạng sáu phần tử mà ISO 32000-1 §8.3.4 định nghĩa cho toán tử cm nối một ma trận vào ma trận biến đổi hiện hành. Cặp (a, b) là hàng đầu tiên, (c, d) là hàng thứ hai, và (e, f) là hàng tịnh tiến. Thói quen vector cột học từ OpenGL hay từ một khóa đại số tuyến tính sẽ đánh lừa bạn ở đây, và chúng sẽ đánh lừa một cách âm thầm, vì một ma trận sai thứ tự vẫn là một ma trận hoàn toàn hợp lệ. Đọc một tổ hợp theo quy ước hàng từ trái sang phải và thứ tự áp dụng sẽ tự lộ ra: vì point * (M * Op) bằng (point * M) * Op, một thao tác được nối thêm tác động lên các tọa độ mà ma trận hiện có đã tạo ra, tức là trong không gian trang, trong khi một thao tác được chèn trước tác động trước khi ma trận hiện có chạy, trong chính không gian đầu vào của đối tượng
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 mặc định là theo chiều kim đồng hồ và tính bằng độ, với ACounterClockwise và AAngleInRadians khả dụng khi dữ liệu nguồn của bạn mang dấu theo hướng khác. Các property chỉ-đọc a tới f và property Handle trao lại cho bạn FS_MATRIX thô, đây chính là thứ mà FPDFPageObj_SetMatrix muốn. Không có gì trong lớp này giấu sáu con số khỏi bạn cả, và điều đó có chủ đích: khi một phép biến đổi hoạt động sai, in ra a tới f là cách chẩn đoán nhanh nhất bạn có
Vì sao chèn trước một phép tịnh tiến lại cần phần tuyến tính?
Vì một phép dịch chuyển được chèn trước được viết trong không gian đầu vào của ma trận, và nó phải được mang qua phần tuyến tính hiện hành trước khi có thể gia nhập hàng tịnh tiến. Vì vậy TPdfMatrix.PreTranslate tính e := dx*a + dy*c + e và f := dx*b + dy*d + f. Nối thêm là hướng dễ dàng: TPdfMatrix.Translate được viết trong không gian trang, nơi không có gì cần chuyển đổi, nên nó chỉ cộng dx vào e và dy vào f. Bất cứ ai "tối ưu hóa" PreTranslate xuống còn hai phép cộng vừa mới xóa mất phần xoay và co dãn khỏi phép dịch chuyển đó
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;
Cùng sự bất đối xứng đó xuyên suốt cặp co dãn, và đáng biết mỗi cái chạm vào những phần tử nào trước khi bạn debug một cái vào lúc ba giờ sáng. TPdfMatrix.PreScale nhân các hàng, co dãn a và b theo scaleX và c cùng d theo scaleY, và nó để yên phần tịnh tiến vì phép dịch chuyển đã xảy ra ở phía sau rồi. TPdfMatrix.Scale mang tính nối thêm thì nhân các cột thay vào đó, nhân a, c, e theo scaleX và b, d, f theo scaleY, nên offset hiện có co dãn cùng với mọi thứ khác. Cả hai đều là những đường đi mục đích đơn bỏ qua tích sáu phần tử tổng quát, và cả hai đều bảo toàn đúng ngữ nghĩa kết hợp của dạng tổng quát
Hai phép tịnh tiến nằm ở đâu trong một phép xoay quanh tâm?
Bao quanh thao tác đó, không phải bao quanh toàn bộ ma trận, và theo đúng thứ tự này. TPdfMatrix.RotateAt nối thêm Translate(-pivot), rồi phép xoay, rồi Translate(+pivot), dưới quy ước vector hàng điều này kết hợp thành Translate(-pivot) * Op * Translate(pivot). Chuỗi đó chính là điều giữ cho điểm tâm cố định dưới thao tác mới trong khi vẫn để ma trận hiện có tạo ra tọa độ của nó trước rồi mới trao đi. Viết cặp đó theo chiều ngược lại, như cách nó sẽ đúng trong một thư viện vector cột, và đối tượng sẽ bay quanh gốc tọa độ thay vì xoay tại chỗ, đây chính xác là cách một watermark ở giữa lại kết thúc ở ngoài 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;
Cùng cách kết hợp đó hậu thuẫn ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, và CentralFlipAt, nên một khi bạn tin tưởng mẫu hình này cho phép xoay, bạn có thể tin tưởng nó cho phần còn lại. TPdfMatrix.CentralFlip đáng được nêu riêng: nó đảo dấu cả sáu phần tử để cho bạn một vòng quay 180 độ mà không cần lượng giác nào cả, nghĩa là không có cos của một giá trị lẽ ra phải chính xác bằng không và không có trôi dạt tích lũy khi bạn áp dụng nó trong một vòng lặp. Nếu bạn đang đặt các dấu lặp lại thay vì xoay một cái, cơ chế của chính việc đặt đó được trình bày trong con dấu trang tái sử dụng với Form XObject, và công việc ma trận ở đây nằm ngay trên đó
TryDecompose cho bạn biết điều gì về một ma trận?
TPdfMatrix.TryDecompose báo cáo phép tịnh tiến, co dãn, xoay, cắt xiên (shear), định thức và một cờ phản chiếu dưới một quy ước co-dãn-rồi-xoay, và nó báo cáo chúng đủ trung thực để hữu ích cho các quyết định chứ không chỉ để ghi log. ScaleX tới từ độ dài của hàng đầu tiên, Sqrt(a*a + b*b), nên nó luôn dương. ScaleY khi đó là Determinant / ScaleX, khiến nó có dấu. Phép xoay tới từ ArcTan2(-b, a) tính bằng độ, và shear tới từ tích vô hướng của hai hàng được chuẩn hóa bởi cả hai tỷ lệ
Cái dấu trên ScaleY đó chính là phần người ta hay xóa mất, và xóa nó là một lỗi thật sự chứ không phải chỉ mang tính thẩm mỹ. Một định thức âm nghĩa là ma trận chứa một phép phản chiếu. Ép cả hai hệ số co dãn thành dương để các con số trông gọn gàng hơn và bạn đã vứt bỏ mất phép phản chiếu, nên một ma trận được dựng lại từ phép phân tách sẽ quay lại bị soi gương: văn bản đọc ngược, một trang đã quét bị lật, một logo đã nhập quay sai hướng. Trường IsReflected tồn tại để bạn không bao giờ phải tự suy luận điều đó. Đây cũng chính là phép kiểm tra ngăn lỗi xoay-kép kinh điển, nơi mã lệnh cộng thêm một vòng xoay hiển thị vào một trang đã mang sẵn một vòng xoay; phiên bản phía viewer của vấn đề đó được xử lý trong fit thumbnail, zoom, và xoay kép
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;
Khớp một hình chữ nhật vào một hình chữ nhật khác mà không cần đoán
TPdfMatrix.TryCreateRectMapping tự xây ma trận nguồn-sang-đích cho bạn và nhận một TPdfMatrixFitMode là pmfStretch, pmfContain, hay pmfCover. Nó chuẩn hóa cả hai hình chữ nhật trước tiên, vì các hình chữ nhật PDF không bắt buộc phải tới với trái dưới phải hay dưới dưới trên, rồi suy ra các tỷ lệ X và Y độc lập: pmfStretch giữ chúng độc lập, pmfContain lấy cái nhỏ hơn và canh giữa phần letterbox, pmfCover lấy cái lớn hơn và canh giữa phần bị cắt. MapRectToRect đi kèm nối thêm cùng phép ánh xạ đó lên một ma trận hiện có, và NewRectMapping ném ra EPdfMatrixError ở nơi dạng Try trả về False. Đây là nguyên thủy đứng sau mỗi lần đặt ô trong dàn trang N-up và sắp xếp lại trang, nơi mỗi trang nguồn phải rơi vào đúng một ô đã tính mà không cần bạn suy lại phép toán cho từng bố cục
Ma trận suy biến và đường thất bại trung thực
Đầu vào hữu hạn không bảo đảm một kết quả hữu hạn, nên mã khớp hình tính toán bằng Double rồi kiểm tra lại ứng viên Single đã thu hẹp về tính hữu hạn trước khi công bố nó; một phép ánh xạ chứa một vô cực không bao giờ được trao lại như thể nó hợp lệ. Cùng kỷ luật đó chi phối việc nghịch đảo. TPdfMatrix.TryGetInverse từ chối một ma trận dùng một ngưỡng tương đối, so sánh định thức với epsilon nhân bình phương phần tử tuyến tính lớn nhất thay vì với một hằng số cố định, đây chính là điều giữ cho phép kiểm tra có ý nghĩa bất kể đơn vị của bạn là point hay micromet. TryDecompose cũng thoát ra theo cùng cách, từ chối khi độ dài hàng đầu tiên hoặc ScaleY suy ra rơi xuống bằng hoặc dưới epsilon
Hãy chọn kiểu thất bại phù hợp với nơi gọi thay vì bọc mọi thứ trong try-except theo thói quen. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds và TryCreateRectMapping trả về False và để nguyên đích của chúng, phù hợp với việc kiểm tra trúng đích (hit-testing) và các vòng lặp theo từng đối tượng nơi một đối tượng suy biến nên bị bỏ qua, không phải gây tử vong. Invert, InverseCopy, InverseTransformPoint, MapRectToRect và TransformBounds ném ra EPdfMatrixError thay vào đó, phù hợp với mã thiết lập nơi một ma trận suy biến nghĩa là bên gọi đã tính sai điều gì đó. Với công việc hàng loạt, TransformPoints và TransformRects cấp phát mảng kết quả của chúng đúng một lần, TransformPointsInPlace và TransformRectsInPlace tái sử dụng bộ lưu trữ của bạn, và TryTransformBounds tích lũy hộp bao (bounding box) trong một lượt duy nhất thay vì phải vật chất hóa các điểm đã biến đổi trước
Không điều gì trong số này là toán học kỳ lạ cả. Đó là một quy ước, được áp dụng nhất quán, với API được đặt tên sao cho quy ước đó hiển hiện tại nơi gọi: Multiply và các động từ thuần túy nối thêm, họ Pre chèn trước, họ At bao quanh thao tác bằng cặp tâm xoay của nó. Hãy viết thứ tự đó ra trong một comment cạnh bất kỳ tổ hợp nào bạn xây dựng, vì đoạn mã đọc đúng hôm nay là đoạn mã mà ai đó sẽ đảo ngược trong sáu tháng nữa. Tài liệu tham khảo đầy đủ về TPdfMatrix, cùng với các API đối tượng trang và render mà những phép biến đổi này nuôi vào, nằm cùng PDFium Component cho Delphi và C++Builder