PDF 아핀 행렬은 ISO 32000-1 §8.3.3의 행 벡터 관례를 사용하며, 여기서 점은 행렬의 왼쪽에서 곱해집니다: point' = point * M. 델파이와 C++Builder용 PDFium Component에서는 그 한 가지 사실이 TPdfMatrix의 API 표면 전체를 결정합니다: Multiply는 append하므로 M := M * Op이고, PreMultiply는 prepend하므로 M := Op * M입니다
모든 전형적인 변환 버그는 그 문장이 거꾸로 기억된 데서 비롯됩니다. 테스트 파일에서는 깔끔하게 회전하지만 고객 파일에서는 페이지 절반 밖으로 나가버리는 워터마크. 페이지가 이미 4분의 1 회전을 가지고 있었기 때문에 두 번 회전되어 나오는 썸네일. A4에서는 완벽한 오프셋이 Letter에서는 어긋나는 스탬프. 이들은 렌더링 버그가 아니라 곱셈 순서 버그이며, 각 연산이 어느 공간에 쓰여 있는지를 소리 내어 말할 수 있게 되면 모두 고칠 수 있습니다
규칙을 정하는 행 벡터 관례
TPdfMatrix는 스펙이 이름 붙인 여섯 요소를 저장하고 포맷이 정의하는 그대로 정확히 적용하므로, 추론은 바로 그 변환 자체에서 시작됩니다. TPdfMatrix.TransformPoint는 x' = x*a + y*c + e와 y' = x*b + y*d + f를 계산하는데, 이는 현재 변환 행렬에 행렬을 결합하는 cm 오퍼레이터에 대해 ISO 32000-1 §8.3.4가 정의하는 여섯 요소 형식입니다. 쌍 (a, b)는 첫 번째 행, (c, d)는 두 번째 행, (e, f)는 이동 행입니다. OpenGL이나 선형대수 강의에서 익힌 열 벡터 습관은 여기서 여러분을 오도할 것이며, 그것도 조용히 오도할 것입니다. 잘못된 순서의 행렬도 여전히 완벽하게 유효한 행렬이기 때문입니다. 합성체를 행 관례로 왼쪽에서 오른쪽으로 읽으면 적용 순서가 저절로 나옵니다: point * (M * Op)가 (point * M) * Op와 같으므로, append된 연산은 기존 행렬이 이미 만들어낸 좌표, 즉 페이지 공간에 작용하는 반면, prepend된 연산은 기존 행렬이 실행되기 전, 즉 객체 자신의 입력 공간에서 작용합니다
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 속성은 FPDFPageObj_SetMatrix가 원하는 원시 FS_MATRIX를 그대로 돌려줍니다. 이 클래스의 그 무엇도 여섯 숫자를 여러분에게서 숨기지 않으며, 이는 의도적입니다: 변환이 오작동할 때 a부터 f까지를 출력하는 것이 가장 빠른 진단이기 때문입니다
이동을 prepend할 때 선형 부분이 필요한 이유는 무엇인가
prepend된 이동은 행렬의 입력 공간에 쓰이며, 이동 행에 합류하기 전에 현재의 선형 부분을 거쳐야 하기 때문입니다. 그래서 TPdfMatrix.PreTranslate는 e := dx*a + dy*c + e와 f := dx*b + dy*d + f를 계산합니다. Append는 쉬운 방향입니다: TPdfMatrix.Translate는 변환할 것이 없는 페이지 공간에 쓰이므로, e에 dx를, f에 dy를 더할 뿐입니다. 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로 스케일하며, 이동은 그대로 둡니다. 이동은 이미 그 다음 단계에서 일어났기 때문입니다. Append하는 TPdfMatrix.Scale은 대신 열을 곱해서 a, c, e를 scaleX로, b, d, f를 scaleY로 취하므로, 기존 오프셋도 다른 모든 것과 함께 스케일됩니다. 둘 다 일반적인 여섯 요소 곱을 건너뛰는 단일 목적 경로이며, 둘 다 일반형의 합성 의미론을 정확히 보존합니다
피벗 회전에서 두 개의 이동은 어디로 가는가
전체 행렬 주위가 아니라 연산 주위로, 그리고 그 순서대로입니다. TPdfMatrix.RotateAt은 Translate(-pivot)을 append하고, 그 다음 회전을, 그 다음 Translate(+pivot)을 append합니다. 이는 행 벡터 관례 아래에서 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도 회전을 주기 위해 여섯 요소 전부를 부정하는데, 이는 정확히 0이어야 했을 값의 cos도, 루프 안에서 적용할 때의 누적 드리프트도 없다는 뜻입니다. 하나를 돌리는 것이 아니라 반복된 마크를 배치하고 있다면, 그 배치 자체의 메커니즘은 Form XObject를 사용한 재사용 가능한 페이지 스탬프에서 다루며, 여기서의 행렬 작업은 그 바로 위에 놓입니다
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은 여러분을 위해 소스-투-목적지 행렬을 만들며 pmfStretch, pmfContain, pmfCover 중 하나의 TPdfMatrixFitMode를 받습니다. PDF 사각형은 왼쪽이 오른쪽보다 아래에 있거나 아래쪽이 위쪽보다 아래에 있을 필요가 없으므로, 먼저 두 사각형을 정규화한 다음 독립적인 X와 Y 스케일을 도출합니다: pmfStretch는 둘을 독립적으로 유지하고, pmfContain은 더 작은 쪽을 취해 레터박스를 중앙에 놓으며, pmfCover는 더 큰 쪽을 취해 크롭을 중앙에 놓습니다. 함께 쓰이는 MapRectToRect는 같은 매핑을 기존 행렬에 append하며, NewRectMapping은 Try 형태가 False를 반환하는 지점에서 EPdfMatrixError를 던집니다. 이것은 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와 평범한 동사들은 append하고, Pre 계열은 prepend하며, At 계열은 연산을 자신의 피벗 쌍으로 괄호 칩니다. 여러분이 만드는 어떤 합성체든 그 옆 주석에 순서를 적어두십시오. 오늘 올바르게 읽히는 코드가 여섯 달 뒤 누군가 거꾸로 뒤집는 코드이기 때문입니다. 이 변환들이 공급하는 페이지 객체 및 렌더링 API와 함께 TPdfMatrix의 전체 레퍼런스는 델파이와 C++Builder용 PDFium Component에 있습니다