Artikel Teknis

Pemirsa PDF untuk Lazarus dan Free Pascal dengan PDFium

Delphi dan Lazarus mengompilasi Object Pascal yang sama, dan kemiripan di permukaan itulah yang membuat memindahkan sebuah viewer di antara keduanya jadi menipu. Kedua toolchain ini berbeda pada tiga titik yang penting bagi pekerjaan PDF: tipe string native adalah UTF-16 di Delphi dan UTF-8 pada sebuah aplikasi LCL; VCL dan LCL adalah framework visual yang berbeda dengan kontrol, dialog, dan format form-streaming miliknya sendiri; dan sebuah binary Delphi menargetkan Windows sementara sebuah binary FPC bisa jadi ditujukan untuk Linux atau macOS. Tidak satu pun dari perbedaan itu muncul saat waktu kompilasi. Sebuah viewer yang dibangun di atas PDFium Component, yang menyertakan edisi VCL dan LCL dari satu pohon sumber tunggal, akan berkompilasi bersih di bawah Lazarus setelah sejumlah kecil pertukaran nama unit dan beberapa blok {$IFDEF FPC}. Kegagalannya baru muncul kemudian, ketika data sungguhan dan sebuah deployment sungguhan membongkar asumsi-asumsi yang diam-diam dibuat oleh build Delphi

Empat dari asumsi-asumsi itu menjadi penyebab sebagian besar waktu yang terbuang: encoding teks pada batas UI, godaan untuk memelihara dua salinan form, cara sebuah binary engine native diselesaikan (resolve) saat runtime, dan momen ketika text-to-speech kehabisan platform begitu SAPI tidak ada lagi. Masing-masing murah untuk ditangani jika Anda tahu itu akan datang, dan mahal untuk dilacak jika Anda tidak tahu

Pascal yang sama, payload string yang berbeda

string native milik Delphi telah menjadi UTF-16 sejak 2009. Lazarus dan Free Pascal secara default menggunakan UTF-8 pada aplikasi LCL. API komponen yang berhadapan dengan teks berbicara UTF-16 melalui tipe WString, yang oleh build FPC dialiaskan ke WideString, sehingga setiap batas tempat teks berpindah antara UI LCL Anda dan engine PDF adalah sebuah titik konversi

Konversi terjadi secara otomatis pada penetapan (assignment) yang lugas, dan sebagian besar kode tidak pernah perlu memikirkannya. Dua kebiasaan menjaga bug encoding tetap terjauh. Lewatkan teks apa adanya tanpa manipulasi tingkat byte: kode yang memotong sebuah istilah pencarian berdasarkan offset byte bekerja di Delphi, tempat satu Char adalah satu unit UTF-16, dan merusak UTF-8 multi-byte di LCL. Dan uji dengan data non-ASCII sejak percobaan pertama. Sebuah nama file berbahasa Jerman, sebuah istilah pencarian Sirilik, sebuah nama penulis beraksen dalam metadata dokumen: data uji yang murni ASCII menyembunyikan setiap cacat encoding, karena ASCII adalah satu-satunya rentang tempat UTF-8 dan UTF-16 sepakat byte demi karakter. Bug-nya nyata sepanjang waktu; ASCII hanya membuatnya tak terlihat sampai seorang pelanggan di Munich membuka file yang tidak pernah Anda coba

Diagram batas konversi string UTF-16 dan UTF-8 antara UI penampil LCL dan komponen PDFium di Lazarus
API teks komponen berbicara UTF-16 melalui WString, sehingga UI LCL yang memegang string UTF-8 bertemu titik konversi di setiap batas, dan pemotongan offset byte atau data uji murni ASCII adalah tempat bug encoding bersembunyi

Satu blok kondisional, bukan sebuah fork per IDE

Setelah selusin IFDEF pertama, codebase mulai terasa seperti dua proyek yang mengenakan satu repositori, dan melakukan fork per IDE terlihat menggoda. Itu adalah langkah yang keliru. Perbedaan yang sungguh-sungguh nyata dapat diringkas menjadi satu blok deklarasi bersama, dan sebuah fork melipatgandakan biaya setiap perbaikan bug sejak saat itu. Jagalah lapisan kondisional ini tetap sekecil ini:

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // API teks komponen bersifat UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Semua yang berada di bawah blok itu berkompilasi secara identik di kedua IDE. Penanganan dokumen, navigasi halaman, panggilan rendering: TPdf dan TPdfView mengekspos permukaan yang sama pada edisi VCL dan LCL, sehingga sebagian besar viewer tidak pernah melihat sebuah kondisi compiler. Menjaganya tetap seperti itu adalah disiplin struktural, bukan trik yang cerdik. Logika PDF bersama tinggal dalam unit-unit yang tidak menarik dialog atau panel spesifik-framework apa pun. Sejumlah kecil hal yang sungguh-sungguh berbeda, seperti dialog cetak dan pemilih file dengan konvensi platform masing-masing, tersembunyi di balik sebuah interface tipis yang diimplementasikan satu kali per framework. Blok IFDEF menjadi satu-satunya tempat perbedaan platform di masa depan diizinkan untuk mendarat, alih-alih membiarkan direktif compiler bocor ke empat puluh unit

Bangun form dalam kode, bukan dalam dua designer

Form streaming adalah tempat proyek dual-IDE diam-diam membusuk. Sebuah .dfm dan sebuah .lfm yang mengaku mendeskripsikan form yang sama akan bergeser saling menjauh properti demi properti sampai kedua build berkelakuan berbeda karena alasan yang tidak dapat di-diff oleh siapa pun, karena kedua file itu bahkan tidak berada dalam format yang sama. Membangun viewer saat runtime menghindari seluruh masalah ini. Ada satu urutan constructor, di-version-control sebagai kode biasa, dan ia terbaca sama di kedua platform:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // membuka dokumen; PageCount valid setelah ini
  end;
end;

Urutan pasti dari penetapan-penetapan itu kurang penting dibanding satu baris yang melakukan pekerjaan sesungguhnya. PdfView.Pdf := Pdf mengikat kontrol visual ke komponen dokumen, dan mulai dari titik itu navigasi halaman melalui PageNumber dan perilaku fit melalui FitMode merespons secara identik di bawah VCL maupun LCL. Satu keanehan lintas-framework layak diketahui sebelum seorang pengguna melaporkannya sebagai bug: menetapkan Zoom secara manual mengembalikan FitMode ke pfmNone pada kedua framework. Jadi jika toolbar Anda memperlakukan "fit width" sebagai sebuah preferensi yang melekat (sticky), Anda harus menetapkan ulang fit mode setelah zoom programatis apa pun, atau preferensi itu akan diam-diam berhenti melekat begitu kode pertama kali menyentuh level zoom

Binary yang tidak pernah diperingatkan oleh IDE kepada Anda

Komponen ini membungkus engine PDFium, yang disertakan sebagai sebuah binary platform native, dan binary itulah sumber dari hampir setiap laporan "bekerja di IDE, gagal dari shortcut yang terpasang". Tiga aturan menjadi penyebab sebagian besarnya. Bitness harus cocok persis. Sebuah executable 32-bit tidak dapat memuat pustaka pdfium 64-bit, dan pesan yang dikembalikan oleh OS ("module not found" pada beberapa versi Windows) secara aktif menyesatkan, karena file itu sesungguhnya ada tepat di sebelah executable-nya. Selesaikan path pustaka secara relatif terhadap executable, jangan pernah terhadap working directory; sebuah peluncuran dari IDE dan sebuah peluncuran dari shell berbeda persis pada titik itu, yang menjelaskan mengapa bug ini bersembunyi selama pengembangan. Dan tangkap kegagalan pemuatan sebelum dokumen pertama dibuka, lalu laporkan itu dengan path dan arsitektur yang diharapkan dieja secara jelas. Sebuah tiket dukungan yang berbunyi "PDFium 64-bit binary missing at <path>" selesai dalam hitungan menit. Yang berbunyi "viewer crashes on startup" berubah menjadi satu minggu bolak-balik

Beri versi pada binary engine itu berdampingan dengan executable-nya selagi Anda mengerjakan ini. PDFium bergerak cepat, dan sebuah installer yang memperbarui aplikasi tetapi meninggalkan pustaka yang basi di disk menghasilkan crash yang tidak dapat direproduksi oleh siapa pun di kantor Anda, dengan alasan sederhana bahwa setiap mesin di kantor Anda kebetulan memegang pasangan yang cocok. Perlakukan pustaka itu sebagai bagian dari artefak build, dengan installer yang sama, stempel versi yang sama, dan jalur rollback yang sama seperti executable yang memuatnya

Diagram tiga aturan pemuatan biner native PDFium untuk executable penampil PDF Lazarus atau Delphi
Tiga aturan menutup sebagian besar laporan jalan-di-IDE-gagal-saat-terpasang: executable dan library PDFium harus berbagi satu bitness, path library diselesaikan dari executable alih-alih direktori kerja, dan load yang gagal tertangkap dengan path yang diharapkan dieja jelas

Mendaftarkan komponen di IDE Lazarus

Konstruksi saat runtime sama sekali tidak membutuhkan registrasi design-time, yang merupakan setup paling bersih untuk sebuah viewer yang membangun UI-nya sendiri dalam kode. Ketika Anda memang menginginkan komponen-komponen itu berada di palet Lazarus untuk pekerjaan design-time, install paketnya dan biarkan unit registrasi khususnya, PDFiumLazReg di Lib/FPC/PDFiumLaz.lpk, yang menanganinya. Unit itu ditandai design-time dengan sengaja: ia merujuk pada interface property-editor milik IDE yang tidak boleh pernah ikut ter-link ke dalam executable yang Anda kirimkan

Salah menangani ini, dan gejalanya adalah sebuah aplikasi yang secara tak terjelaskan bergantung pada paket-paket IDE, yang muncul sebagai kegagalan deployment pada mesin pelanggan pertama yang belum pernah memasang Lazarus

Speech dan screen reader di luar Windows

Text-to-speech adalah satu-satunya fitur tempat kisah lintas-platform ini pecah, dan ia pecah pada sistem operasi, bukan pada komponennya. SAPI, backend TTS yang biasa digunakan di Windows, hanya ada di Windows. Sebuah build Lazarus yang masih menargetkan Windows mempertahankan output SAPI penuh dan perilaku kompatibel-NVDA yang sama seperti yang dimiliki versi Delphi aslinya, sehingga sebuah porting Windows-ke-Windows tidak kehilangan apa pun di sini, dan seorang pengguna NVDA tidak dapat membedakan kedua build tersebut

Target Linux atau macOS adalah persoalan yang berbeda. Tidak ada SAPI yang bisa dipanggil, sehingga output audio harus disambungkan ulang ke sebuah layanan speech native sementara API pembacaan di atasnya tetap di tempatnya. Pemisahan itulah argumen untuk menempatkan speech di balik sebuah interface sejak commit pertama: analisis urutan-baca dan kursor pelacak-kata bersifat netral-platform dan terbawa tanpa perubahan, dan hanya lapisan tipis yang benar-benar menghasilkan suara yang harus berubah per platform. Artikel pembaca yang mudah diakses membahas mesin pembacaan itu secara mendalam

Checklist paritas sebelum Anda menyatakan porting selesai

Pemeriksaan berikut ini telah menangkap regresi-regresi nyata, terdaftar kurang lebih dalam urutan kemunculan kegagalan pada umumnya. Buka sebuah dokumen yang path-nya mengandung karakter non-ASCII. Cari sebuah istilah dengan karakter non-ASCII dan pastikan hasilnya di-highlight di tempat yang seharusnya. Latih scroll roda mouse, seleksi drag, dan navigasi halaman keyboard pada setiap kumpulan widget yang Anda kirimkan, karena penanganan focus dan perilaku roda adalah sudut LCL yang paling bergantung pada kumpulan widget. Periksa rendering pada penskalaan tampilan 100%, 150%, dan 200%. Terakhir, jalankan build yang terpasang, bukan build IDE, pada sebuah mesin yang belum pernah memiliki IDE, karena itu adalah satu-satunya uji yang benar-benar menguji resolusi binary secara jujur. Semua yang lain bisa lolos sementara yang satu ini diam-diam gagal

Throughput rendering terbawa di antara kedua edisi tanpa perubahan, sehingga pendekatan caching dari artikel render cache dan performa zoom berlaku bagi viewer LCL persis seperti yang ditulis untuk viewer VCL

Tidak ada satu pun dari ini yang membuat edisi LCL menjadi edisi kelas dua. Permukaan intinya identik di kedua sisi: TPdf, TPdfView, rendering, form, ekstraksi teks, dan API aksesibilitas berperilaku sama tidak peduli IDE mana yang mengompilasinya. Setiap perbedaan yang layak dilacak terikat pada platform, bukan terikat pada edisi. Speech SAPI hanya untuk Windows, dialog mengikuti konvensi masing-masing framework, dan binary harus cocok dengan arsitektur tempat ia dimuat. Kerjakan batas-batas encoding, form runtime, dan resolusi binary dengan benar, dan sisa dari porting ini adalah pekerjaan mekanis yang sudah ditangani oleh compiler untuk Anda

Diagram PDFium Component tentang pemisahan antarmuka suara yang memindahkan keluaran TTS ke balik mesin per-platform dalam penampil PDF Lazarus
Analisis urutan baca dan kursor pelacak kata tetap netral platform sementara satu antarmuka suara tipis bermuara ke SAPI di Windows dan ke layanan suara native di Linux dan macOS

Edisi VCL dan LCL yang dijelaskan di sini dikirimkan bersama sebagai PDFium Component, dengan source code dan API publik yang identik untuk Delphi, C++Builder, dan Lazarus/FPC