Artikel Teknis

Matrix Prepend vs Append PDFium di Delphi: Rotasi Pivot

Matriks affine PDF memakai konvensi row-vector dari ISO 32000-1 §8.3.3, tempat sebuah titik mengalikan matriks dari kiri: point' = point * M. Dalam PDFium Component untuk Delphi dan C++Builder, satu fakta itu menetapkan seluruh permukaan API TPdfMatrix: Multiply melakukan append, sehingga M := M * Op, sementara PreMultiply melakukan prepend, sehingga M := Op * M

Setiap bug transform klasik bermuara pada kalimat itu yang diingat secara terbalik. Watermark yang berputar rapi di berkas test Anda dan mendarat setengah keluar halaman di berkas pelanggan. Thumbnail yang keluar berputar dua kali karena halamannya sudah membawa seperempat putaran. Stamp yang offset-nya sempurna di A4 dan melenceng di Letter. Tidak satu pun dari itu adalah bug rendering; mereka adalah bug urutan perkalian, dan semuanya bisa diperbaiki begitu Anda bisa menyebut dengan lantang ruang mana yang menjadi tempat penulisan setiap operasi

Konvensi row-vector yang menetapkan aturannya

TPdfMatrix menyimpan enam elemen bernama-spesifikasi dan menerapkannya persis seperti yang didefinisikan formatnya, sehingga transform itu sendiri adalah tempat penalaran dimulai. TPdfMatrix.TransformPoint menghitung x' = x*a + y*c + e dan y' = x*b + y*d + f, yaitu bentuk enam-elemen yang didefinisikan ISO 32000-1 §8.3.4 untuk operator cm yang mengonkatenasi sebuah matriks ke matriks transformasi saat ini. Pasangan (a, b) adalah baris pertama, (c, d) baris kedua, dan (e, f) baris translasi. Kebiasaan column-vector yang dibawa dari OpenGL atau dari sebuah kursus aljabar linear akan menyesatkan Anda di sini, dan mereka akan menyesatkan Anda secara diam-diam, karena sebuah matriks dengan urutan yang salah tetap merupakan matriks yang sepenuhnya valid. Baca sebuah komposit dalam konvensi row dari kiri ke kanan dan urutan penerapannya jatuh dengan sendirinya secara gratis: karena point * (M * Op) sama dengan (point * M) * Op, sebuah operasi yang di-append bekerja pada koordinat yang sudah dihasilkan matriks yang ada, yaitu pada ruang halaman, sementara sebuah operasi yang di-prepend bekerja sebelum matriks yang ada berjalan, dalam ruang input milik objeknya sendiri

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 default-nya searah jarum jam dan dalam derajat, dengan ACounterClockwise dan AAngleInRadians tersedia ketika data sumber Anda bertanda sebaliknya. Property a hingga f yang read-only dan property Handle memberi Anda kembali FS_MATRIX mentah, yaitu yang diinginkan FPDFPageObj_SetMatrix. Tidak ada apa pun dalam class itu yang menyembunyikan keenam angka itu dari Anda, dan itu disengaja: ketika sebuah transform berperilaku salah, mencetak a hingga f adalah diagnosis tercepat yang Anda miliki

Kenapa prepend sebuah translasi membutuhkan bagian linear?

Karena sebuah pergeseran yang di-prepend ditulis dalam ruang input matriks, dan ia harus dibawa melalui bagian linear saat ini sebelum bisa bergabung dengan baris translasi. TPdfMatrix.PreTranslate karena itu menghitung e := dx*a + dy*c + e dan f := dx*b + dy*d + f. Append adalah arah yang mudah: TPdfMatrix.Translate ditulis dalam ruang halaman, tempat tidak ada yang perlu dikonversi, sehingga ia hanya menambahkan dx ke e dan dy ke f. Siapa pun yang "mengoptimalkan" PreTranslate turun menjadi dua penjumlahan baru saja menghapus rotasi dan skala dari pergeseran itu

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;

Asimetri yang sama berjalan melalui pasangan scale, dan itu layak diketahui elemen mana yang disentuh masing-masing sebelum Anda men-debug salah satunya jam tiga pagi. TPdfMatrix.PreScale mengalikan baris, menskalakan a dan b dengan scaleX dan c serta d dengan scaleY, dan membiarkan translasi tidak tersentuh karena pergeserannya sudah terjadi di hilir. TPdfMatrix.Scale yang append justru mengalikan kolom, mengambil a, c, e dengan scaleX dan b, d, f dengan scaleY, sehingga offset yang sudah ada ikut terskala bersama segala sesuatu yang lain. Keduanya adalah jalur bertujuan-tunggal yang melewati produk enam-elemen umum, dan keduanya mempertahankan semantik komposisi dari bentuk umum secara persis

Ke mana kedua translasi itu pergi dalam sebuah rotasi pivot?

Di sekeliling operasinya, bukan di sekeliling seluruh matriks, dan dalam urutan itu. TPdfMatrix.RotateAt meng-append Translate(-pivot), lalu rotasinya, lalu Translate(+pivot), yang di bawah konvensi row-vector berpadu sebagai Translate(-pivot) * Op * Translate(pivot). Urutan itulah yang menjaga pivot tetap tidak berubah di bawah operasi yang baru sementara tetap membiarkan matriks yang ada menghasilkan koordinatnya lebih dulu lalu meneruskannya. Tulis pasangan itu terbalik, seperti yang akan benar dalam sebuah pustaka column-vector, dan objeknya mengorbit origin alih-alih berputar di tempat, yang persis merupakan cara sebuah watermark yang terpusat berakhir di luar 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;

Komposisi yang sama mendukung ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, dan CentralFlipAt, sehingga begitu Anda mempercayai pola ini untuk rotasi, Anda bisa mempercayainya untuk yang lain. TPdfMatrix.CentralFlip layak disorot tersendiri: ia menegasikan keenam elemennya untuk memberi Anda putaran 180 derajat tanpa trigonometri sama sekali, yang berarti tidak ada cos dari sebuah nilai yang seharusnya persis nol dan tidak ada drift yang terakumulasi ketika Anda menerapkannya dalam sebuah loop. Bila Anda menempatkan tanda yang berulang alih-alih memutar satu, mekanisme penempatannya sendiri dibahas di stamp halaman yang bisa dipakai ulang dengan Form XObject, dan pekerjaan matriks di sini berada langsung di atasnya

Apa yang diberitahukan TryDecompose soal sebuah matriks?

TPdfMatrix.TryDecompose melaporkan translasi, skala, rotasi, shear, determinan, dan sebuah flag refleksi di bawah konvensi scale-lalu-rotasi, dan ia melaporkannya cukup jujur untuk berguna bagi keputusan, bukan sekadar untuk logging. ScaleX berasal dari panjang baris pertama, Sqrt(a*a + b*b), sehingga selalu positif. ScaleY kemudian adalah Determinant / ScaleX, yang membuatnya bertanda. Rotasi berasal dari ArcTan2(-b, a) dalam derajat, dan shear dari dot product kedua baris yang dinormalisasi oleh kedua skalanya

Tanda pada ScaleY itulah bagian yang dihapus orang, dan menghapusnya adalah sebuah bug sungguhan, bukan sekadar kosmetik. Determinan negatif berarti matriksnya mengandung sebuah refleksi. Paksa kedua faktor skala positif agar angkanya terlihat lebih rapi dan Anda sudah membuang refleksinya, sehingga sebuah matriks yang dibangun ulang dari dekomposisi itu kembali dalam keadaan tercermin: teks terbaca terbalik, sebuah halaman hasil scan terbalik, sebuah logo yang diimpor menghadap arah yang salah. Field IsReflected ada sehingga Anda tidak pernah perlu menyimpulkannya. Ini juga pemeriksaan yang mencegah rotasi ganda yang klasik, tempat kode menambahkan sebuah putaran tampilan ke sebuah halaman yang sudah membawa satu; versi sisi-viewer dari masalah itu dibahas tuntas di fit thumbnail, zoom, dan rotasi ganda

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;

Menyesuaikan satu rectangle ke rectangle lain tanpa menebak

TPdfMatrix.TryCreateRectMapping membangun matriks source-ke-destination untuk Anda dan mengambil sebuah TPdfMatrixFitMode berupa pmfStretch, pmfContain, atau pmfCover. Ia menormalisasi kedua rectangle lebih dulu, karena rectangle PDF tidak diwajibkan datang dengan left di bawah right atau bottom di bawah top, lalu menurunkan skala X dan Y yang independen: pmfStretch mempertahankan keduanya independen, pmfContain mengambil yang lebih kecil dan memusatkan letterbox-nya, pmfCover mengambil yang lebih besar dan memusatkan crop-nya. MapRectToRect pendampingnya meng-append pemetaan yang sama ke sebuah matriks yang sudah ada, dan NewRectMapping memunculkan EPdfMatrixError di tempat bentuk Try-nya mengembalikan False. Ini adalah primitif di balik setiap penempatan cell dalam imposisi N-up dan penyusunan ulang halaman, tempat setiap halaman sumber harus mendarat di dalam sebuah cell yang dihitung tanpa Anda menurunkan ulang aritmatikanya per layout

Matriks degenerate dan jalur kegagalan yang jujur

Input yang finite tidak menjamin hasil yang finite, sehingga kode fitting-nya menghitung dalam Double lalu memeriksa ulang kandidat Single yang sudah dipersempit untuk finiteness sebelum menerbitkannya; sebuah pemetaan yang mengandung sebuah infinity tidak pernah diserahkan kembali seolah-olah valid. Disiplin yang sama mengatur inversi. TPdfMatrix.TryGetInverse menolak sebuah matriks memakai sebuah ambang relatif, membandingkan determinannya terhadap epsilon dikali kuadrat elemen linear terbesar, bukan terhadap sebuah konstanta tetap, yang menjaga agar tes-nya tetap bermakna baik unit Anda adalah point maupun mikrometer. TryDecompose keluar dengan cara yang sama, menolak ketika panjang baris pertama atau ScaleY yang diturunkan jatuh pada atau di bawah epsilon

Pilih gaya kegagalan yang sesuai dengan call site-nya alih-alih membungkus segala sesuatu dalam try-except karena kebiasaan. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds, dan TryCreateRectMapping mengembalikan False dan membiarkan targetnya tidak tersentuh, yang cocok untuk hit-testing dan loop per-objek tempat sebuah objek degenerate seharusnya dilewati, bukan fatal. Invert, InverseCopy, InverseTransformPoint, MapRectToRect, dan TransformBounds justru memunculkan EPdfMatrixError, yang cocok untuk kode setup tempat sebuah matriks singular berarti caller-nya menghitung sesuatu yang salah. Untuk pekerjaan batch, TransformPoints dan TransformRects mengalokasikan array hasilnya persis sekali, TransformPointsInPlace dan TransformRectsInPlace memakai ulang storage Anda, dan TryTransformBounds mengakumulasi bounding box dalam satu pass alih-alih mewujudkan titik yang sudah ditransformasi lebih dulu

Tidak satu pun dari ini adalah matematika yang eksotis. Ini adalah satu konvensi, diterapkan secara konsisten, dengan API yang dinamai sehingga konvensinya terlihat pada call site-nya: Multiply dan kata kerja polos lainnya melakukan append, keluarga Pre melakukan prepend, keluarga At membungkus operasinya dengan pasangan pivot-nya. Tuliskan urutannya dalam sebuah komentar di samping komposit apa pun yang Anda bangun, karena kode yang terbaca benar hari ini adalah kode yang akan dibalik seseorang enam bulan lagi. Referensi lengkap TPdfMatrix, bersama API page-object dan rendering yang menjadi tujuan transform-transform ini, ada bersama PDFium Component untuk Delphi dan C++Builder