Artikel Teknis

Handle Page Object PDFium Basi Setelah Transform di Delphi

Saat FPDFPage_TransFormWithClip menulis ulang sebuah halaman, setiap handle FPDF_PAGEOBJECT yang sudah Anda pegang tetap menggambarkan hasil parse dari sebelum transform. PDFium Component untuk Delphi dan C++Builder menyelesaikan ini di dalam TransformPageContent, yang membongkar muat text page, meregenerasi konten, lalu memuat ulang halaman sehingga query berikutnya melihat koordinat baru

Gejalanya diam-diam. Anda menerapkan skala 0,9 untuk menambah margin cetak, lalu membaca PageObjectInfo dan mendapat persis angka yang sama seperti sebelum pemanggilan. Tidak ada exception, tidak ada kode error, tidak ada apa pun di log. Ini kegagalan berbeda dari cache text page yang dijelaskan di artikel tentang text page yang basi setelah edit: di sana cache-nya adalah satu handle FPDF_TEXTPAGE yang bisa Anda buang dan bangun ulang, di sini masalahnya adalah setiap handle page object di variabel Anda sendiri, plus satu kelas getter yang melaporkan kegagalan lewat kode kembalian yang dibuang kebanyakan pemanggil

Kenapa batas page object menjadi basi tanpa error?

Karena handle page object adalah pointer ke dalam representasi yang di-parse dari satu content stream tertentu, dan transform level-halaman penuh menggantikan content stream itu dengan yang baru. PDFium tidak menelusuri call stack Anda mencari handle untuk ditambal. Ia membangun graf objek yang baru dan meninggalkan yang lama persis seperti sebelumnya, jadi pembacaan terhadap handle lama adalah pembacaan yang sepenuhnya valid dari struktur yang tidak lagi sesuai dengan apa yang dikatakan file tersebut

ISO 32000-1 §7.8.2 mendefinisikan content stream sebagai urutan operator yang menggambar sebuah halaman, dan §8.3.3 mendefinisikan bagaimana current transformation matrix memetakan user space ke device space. Transform level-halaman diekspresikan dengan membungkus dan menulis ulang operator-operator itu, bukan dengan menyunting koordinat per-objek di tempat. Jadi koordinat yang dibawa objeknya mungkin sama sekali tidak berubah; yang berubah adalah matriks yang berlaku saat mereka digambar. Handle mana pun yang di-parse di bawah matriks lama menjawab pertanyaan geometri di bawah matriks lama, dan menjawabnya tanpa keluhan

Apa sebenarnya yang ditulis ulang FPDFPage_TransFormWithClip

Ia menulis ulang halamannya, bukan snapshot Anda. FPDFPage_TransFormWithClip mengambil FS_MATRIX dan persegi panjang clip FS_RECTF dan menerapkan keduanya ke seluruh konten halaman. Ini adalah pemanggilan yang tepat untuk margin, skala imposisi, dan menormalkan halaman berukuran aneh terhadap kotak target. Ini adalah pemanggilan yang salah untuk diraih jika Anda mengharapkan handle yang ada mengikuti, dan juga layak diingat bahwa ia hanya menyentuh konten halaman: anotasi adalah layer terpisah dan butuh TransformPageAnnotations, yang meneruskan enam koefisien matriks yang sama ke FPDFPage_TransformAnnots

var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot taken before the transform

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // 5% margin, A4 in points
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds still holds pre-transform geometry, and Info.Handle now
  // points into a page that TransformPageContent has already replaced
end;

Urutan refresh yang dipakai TransformPageContent

Empat langkah, dalam urutan ini: bongkar muat text page, transform, generate konten, muat ulang halaman. TPdf.TransformPageContent menjalankan persis urutan itu. Ia memanggil CheckPageActive, menyalin matriks dan clip ke bentuk record native-nya, memanggil UnloadTextPage, lalu FPDFPage_TransFormWithClip, lalu UpdatePage, yang merupakan wrapper di sekitar FPDFPage_GenerateContent, dan terakhir ReloadPage

Setiap langkah memiliki tempatnya. UnloadTextPage berjalan lebih dulu karena FPDF_TEXTPAGE yang di-cache memegang kotak karakter yang dihitung di bawah matriks lama, dan juga menghapus daftar web-link turunannya dan sesi find yang sedang berjalan yang dibangun darinya. FPDFPage_GenerateContent harus berjalan sebelum reload, karena transform-nya hidup di halaman in-memory sampai ia diserialisasi kembali ke content stream, dan reload kalau tidak akan mem-parsing ulang stream yang tidak dimodifikasi. ReloadPage menutup dengan FPDF_LoadPage terhadap indeks halaman saat ini, yang merupakan satu-satunya yang sungguh memberi Anda graf objek yang baru

// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // unload text page, transform,
                                           // generate content, reload page
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle and bounds from the new parse
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

Satu detail di ReloadPage layak dicontek jika Anda pernah menulis urutan ini sendiri. Ia memuat halaman baru lebih dulu dan baru mengkomitnya ke field-nya setelah itu, jadi pemuatan halaman yang gagal meninggalkan halaman native saat ini dan semua cache turunannya utuh alih-alih menjatuhkan Anda ke state setengah-terbongkar. Reload tidak gratis — Anda membayar re-parse penuh halamannya — tapi dibayar sekali per transform, bukan sekali per query, dan tidak ada alternatif benar yang lebih murah

Jangan membawa handle melintasi reload

Setelah reload, handle lama bukan sekadar basi, mereka dangling. FPDF_PAGE sebelumnya sudah ditutup, dan nilai FPDF_PAGEOBJECT yang miliknya adalah pointer ke memori yang sudah dibebaskan. TPdfPageObjectInfo mengekspos handle native di field Handle-nya, yang sungguh berguna untuk meneruskan objek langsung ke pemanggilan level-rendah, dan sama sungguhnya berbahaya untuk disimpan dalam field form atau list melintasi operasi yang memuat ulang halaman. Perlakukan record snapshot sebagai valid hanya sampai pemanggilan berikutnya yang meregenerasi konten, dengan semangat yang sama dengan aturan kepemilikan yang dibahas di catatan tentang ABI dan keamanan memori pada batas PDFium

Bisakah sebuah getter gagal dan tetap terlihat seperti data valid?

Ya, dan ini separuh kedua dari masalah yang sama. FPDFPageObj_GetRotatedBounds dan FPDFPageObj_GetIsActive adalah getter out-parameter: mereka mengembalikan flag sukses int dan menulis jawaban sebenarnya ke argumen referensi. Keduanya bisa mengembalikan FALSE untuk objek yang sudah dibuat tapi halamannya belum di-parse ulang. Saat itu terjadi, parameter out-nya dibiarkan tidak tersentuh, dan record Pascal yang diinisialisasi dengan Default(TPdfPageObjectInfo) semuanya nol, jadi pemanggil melihat sebuah kuadrilateral dengan empat titik di origin dan flag Active False. Sebuah pemanggilan yang gagal diam-diam dipromosikan menjadi data yang terlihat masuk akal

TPdfPageObjectInfo menjawab ini dengan sentinel eksplisit. HasRotatedBounds membawa hasil pemanggilan FPDFPageObj_GetRotatedBounds, HasActiveState membawa hasil FPDFPageObj_GetIsActive, dan field geometri serta state hanya ditulis saat sentinel yang bersangkutan True. Bentuk yang sama berulang di seluruh record untuk getter out-parameter lainnya, jadi HasMatrix, HasFillColor, HasStrokeColor, dan HasStrokeWidth semuanya berarti hal yang sama: pemanggilan native-nya berhasil dan field di sampingnya bermakna

Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds is array [1..4] of TPdfPoint, in draw order
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // the native call failed; fall back to the axis-aligned rectangle
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive

Pola ini menggeneralisasi ke setiap getter PDFium yang mengikuti konvensi kode-kembalian-plus-parameter-out, dan ada banyak sekali. Jika sebuah wrapper melipat konvensi itu menjadi hasil fungsi biasa, ia sudah membuang satu-satunya sinyal yang membedakan "jawabannya nol" dari "tidak ada jawaban". Membawa satu boolean ekstra per field berbiaya satu byte dan menghilangkan seluruh kategori bug di mana record default disalahartikan sebagai pengukuran

Di mana ini masih menggigit

Tiga batasan jujur. Pertama, refresh-nya per halaman: mentransform halaman dua dan handle apa pun yang Anda pegang untuk halaman satu tidak terpengaruh, tapi sekarang Anda punya dua halaman yang di-parse pada waktu berbeda dan terserah Anda mengingat snapshot mana berasal dari mana. Kedua, stabilitas indeks tidak dijamin lintas regenerasi konten — setelah reload, indeks 3 adalah apa pun indeks 3 pada parse yang baru, jadi identifikasi ulang objek berdasarkan tipe dan geometrinya alih-alih mengasumsikan posisinya tetap. Ketiga, persegi panjang clip di FPDFPage_TransFormWithClip diterapkan ke konten halaman dan tidak mengubah ukuran page box mana pun; jika Anda menskalakan konten lebih kecil untuk membuat margin, MediaBox-nya tetap ukuran yang sama seperti selalu, dan viewer akan menunjukkan lembar aslinya dengan gambar yang mengecil di dalamnya. Tidak ada dari ini yang eksotis — ini konsekuensi biasa dari API C yang menyerahkan pointer ke state yang di-parse dan meninggalkan lifetime kepada pemanggilnya. Perbaikannya adalah yang berfungsi di mana pun juga: definisikan persis kapan sebuah snapshot kedaluwarsa, refresh pada batas itu, dan jangan pernah biarkan pemanggilan yang gagal menyamar sebagai nilai

Jika Anda menelusuri perilaku matriks secara lebih umum, urutan perkalian yang menentukan di mana sebuah transform mendarat dibahas di artikel tentang prepend, append, dan pivot dengan matriks. API transform dan page object yang dijelaskan di sini hadir dengan PDFium Component untuk Delphi dan C++Builder, yang halaman produknya membawa referensi lengkap untuk record snapshot page object dan field sentinel-nya