Artikel Teknis

Bandingkan PDF Berdampingan di Delphi dengan PDFium

Dua dokumen terbuka sekaligus, nomor halaman yang sama, masing-masing di dalam panel yang bisa digulir sendiri: itulah inti sebuah viewer pembanding. PDFium Component menghadirkan ini lewat model objek yang lugas, tempat TPdf memiliki file-nya dan TPdfView memiliki tampilannya. Satu dokumen, satu TPdf, satu TPdfView. Anda ingin tiga panel, maka Anda punya tiga pasang. Bagian yang sulit bukanlah panggilan API-nya; melainkan aritmetika tata letak saat jendelanya diubah ukuran dan logika sinkronisasi halaman saat Anda memutuskan view mana yang harus mengikuti view mana

Tata Letak Form

Form VCL-nya memuat tiga container TScrollBox yang berdampingan, masing-masing dengan sebuah TPdfView di dalamnya dan disejajarkan ke alClient supaya memenuhi kotaknya. Dua component TSplitter duduk di antara kotak-kotak itu supaya pengguna bisa menyesuaikan lebar kolomnya saat program berjalan. Sebuah toolbar di atas panelnya membawa tombol buka, kendali zoom, dan sakelar dua-view / tiga-view

Mode tiga view adalah sebuah boolean yang dilacak form itu secara internal. Ketika ia berbalik, Anda menghitung ulang lebarnya lalu menampilkan atau menyembunyikan kolom ketiga. Pendekatan yang paling sederhana adalah membersihkan semua properti Align, menyembunyikan splitter-nya, lalu menyetel posisi absolutnya:

Diagram tata letak form sebuah viewer pembanding PDF berdampingan di Delphi yang dibangun dengan PDFium Component, menampilkan toolbar, tiga scroll box berisi panel TPdfView, dan splitter pada mode dua view maupun tiga view
Setiap panel adalah sebuah scroll box dengan TPdfView di dalamnya, dan beralih antara dua view dan tiga view hanyalah kumpulan penugasan lebar yang berbeda
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Terapkan (ClientHeight - tinggi toolbar) yang sama ke ketiga nilai Height
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Menyetel Align := alNone pada ketiga kotak sebelum aritmetika bilangan bulatnya mencegah mesin constraint VCL bertengkar dengan penugasan Anda. Pulihkan kembali kelihatan-tidaknya splitter setelah pemosisian bila Anda menginginkan penyesuaian ukuran dengan seret pada mode dua view

Tinggi setiap scroll box adalah area klien dikurangi tinggi panel toolbar-nya. Karena toolbar-nya ditambatkan di atas dengan alTop, ClientHeight - PanelButtons.Height memberi Anda ruang vertikal yang bisa dipakai. Tugaskan nilai ini ke ketiga kotak di dalam panggilan UpdateLayout yang sama supaya tidak pernah ada satu frame pun tempat sebuah kotak lebih tinggi daripada yang lain dan menyebabkan tata letaknya berkedip

Membuka Sebuah Dokumen

Setiap pasangan panel membutuhkan prosedur pembukanya sendiri. Polanya pendek: nonaktifkan component-nya, setel nama file-nya, aktifkan, lalu periksa Active; jika ia tetap False, mintalah password lalu coba lagi. Perhatikan bahwa TPdfView.Active adalah yang mengendalikan rendering, sementara TPdf.Active adalah yang benar-benar membuka file-nya; keduanya saling bebas. Menyetel PdfView.Active := True ketika TPdf yang tertaut padanya belum aktif tidak berbahaya namun tidak menampilkan apa pun

Diagram alur pembukaan sebuah dokumen PDF dengan PDFium Component di Delphi, menampilkan pemeriksaan Active yang senyap, satu percobaan ulang berpassword, dan dialog error untuk file yang rusak atau terlindungi password
Pemuatan yang gagal meninggalkan Active bernilai False tanpa melempar apa pun, jadi alurnya memeriksanya, mencoba ulang sekali dengan password, lalu akhirnya melaporkan masalahnya alih-alih menampilkan panel kosong
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // Kegagalan muat itu senyap: Active tetap False alih-alih melempar error.
  if not PdfComponent.Active then
  begin
    // Kemungkinan besar file berpassword; beri pengguna satu kali coba lagi.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

Selalu periksa PdfComponent.Active setelah penugasannya; file yang rusak atau password yang salah membuat pemuatannya gagal secara senyap tanpa melempar exception pada jalur bawaannya. Menyetel PdfViewComponent.PageNumber := 1 secara eksplisit setelah pembukaan yang berhasil mencegah nomor halaman basi dari dokumen sebelumnya

Dialog pesan di akhirnya memang disengaja: Anda ingin file yang rusak atau tidak didukung muncul ke permukaan seketika alih-alih ditelan menjadi panel kosong yang diam. Pengguna yang tidak melihat apa pun tidak punya gambaran apakah file-nya termuat lalu memang kosong, atau apakah component-nya menolaknya. Melaporkan kegagalannya menjaga error-nya tetap terlihat

Pelacakan Panel Aktif

Ketika pengguna mengklik di dalam sebuah panel, panel itu menjadi aktif. Form-nya melacak sebuah field privat FActivePdfView: TPdfView. Umpan balik visualnya adalah perubahan warna batas pada TScrollBox yang memuatnya: setel ke clHighlight untuk yang aktif dan clWindow untuk yang lain. Kaitkan ini ke setiap TPdfView.OnClick dan ke prosedur pembukanya supaya fokusnya mengikuti dokumen yang baru saja Anda buka

Sebagian operasi berlaku untuk semua panel yang terlihat alih-alih hanya yang aktif. Sebuah boolean FAllViewsMode pada form-nya menggerakkan percabangan itu. Ketika ia bernilai true, perubahan zoom dan navigasi halaman menyebar ke setiap panel yang punya dokumen aktif:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Navigasi Halaman Tersinkron

Navigasi tersinkron itu opsional namun berguna untuk workflow revisi dokumen tempat kedua file mencakup rentang halaman yang sama. Logikanya berada di dalam sebuah event handler yang menyala setelah pengguna menavigasi salah satu view. Ketika sebuah view sumber mengubah PageNumber-nya, handler-nya menyebarkan nomor itu ke view yang lain, dengan satu penjaga: view sasarannya harus punya setidaknya sebanyak itu halaman, kalau tidak lewati saja

PageNumber pada TPdfView dan pada TPdf saling bebas. TPdf.PageNumber melacak halaman mana yang dianggap aktif oleh component dokumennya; TPdfView.PageNumber melacak apa yang ditampilkan di layar. Untuk keperluan navigasi, Anda menginginkan properti view-nya, bukan properti dokumennya

Sebuah checkbox berlabel semacam "Sync pages" memberi kendali kepada pengguna. Ketika ia tidak dicentang, setiap panel bernavigasi sendiri-sendiri dan handler-nya langsung keluar. Kebebasan itu penting untuk kasus pemakaian ketika kedua dokumennya punya jumlah halaman berbeda, atau ketika pengguna ingin menemukan bagian yang setara di dalam sebuah terjemahan yang dimulai pada halaman berbeda. Memaksa sinkron selamanya justru akan membuat tool-nya lebih sulit dipakai daripada susunan dua jendela desktop yang sederhana

Satu hal yang perlu diawasi: menyetel PdfView.PageNumber secara terprogram di dalam handler sinkronisasinya akan memicu event perubahan pada view itu sendiri. Jagalah dari rekursi tak berujung dengan sebuah flag boolean yang Anda setel sebelum penugasan lalu Anda bersihkan segera sesudahnya. Flag itu berlaku per form, bukan per view, karena ketiga view berbagi handler yang sama

Diagram navigasi halaman tersinkron pada sebuah viewer pembanding PDF Delphi yang memakai PDFium Component, dengan checkbox sinkronisasi, penjaga jumlah halaman per view sasaran, dan flag penjaga rekursi
Nomor halamannya berjalan dari view sumber ke setiap view lain hanya ketika sinkronisasinya dinyalakan dan setiap view sasaran benar-benar memuat halaman itu

Zoom Per Panel

Setiap TPdfView membawa properti Zoom-nya sendiri, sebuah Double dalam persen tempat Zoom := 100 berarti ukuran sebenarnya (100%). Menyetelnya menimpa FitMode apa pun yang sedang aktif. Untuk sebuah tombol paskan-ke-lebar pada panel yang aktif, baca zoom paskannya dari PdfView.PageWidthZoom[PdfView.PageNumber] lalu tugaskan. Untuk paskan-ke-halaman, pakai PageZoom[PageNumber]. Keduanya adalah properti array yang diindeks dengan nomor halaman berbasis 1, jadi jagalah dari nomor halaman nol sebelum mengaksesnya

Ketika Anda mengekspor halaman aktif ke sebuah gambar, baca rotasinya dari view-nya namun panggil RenderPage pada component TPdf, bukan pada view-nya. Bentuk bitmap dari TPdf.RenderPage menerima dimensi piksel yang eksplisit ditambah sebuah nilai TRotation dan sebuah himpunan TRenderOptions. Varian fungsinya mengembalikan sebuah TBitmap yang dimiliki pemanggil dan Anda bebaskan sendiri setelah menyimpannya:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

Pengali 2x pada lebar dan tingginya memberi keluaran yang lebih tajam untuk dokumen berteks halus. try/finally di sekeliling pembebasan bitmap-nya bukanlah pilihan; sebuah pembatalan TSaveDialog tetap mendarat di blok finally, dan Anda ingin bitmap-nya dilepas apa pun yang dilakukan pengguna

Kebutuhan DLL

PDFium Component membungkus library pdfium native. Proses host 32 bit membutuhkan pdfium32.dll; host 64 bit membutuhkan pdfium64.dll. Varian dengan engine JavaScript V8 menambahkan akhiran v8 dan berbobot kira-kira 23-27 MB dibanding build standar yang 5-6 MB. Untuk sebuah viewer pembanding yang menonaktifkan pengisian form (Pdf.FormFill := False), build standar tanpa V8 sudah memadai dan menjaga distribusinya tetap kecil

Tempatkan DLL-nya di direktori yang sama dengan executable-nya, atau di direktori mana pun yang ada pada PATH sistem. Component-nya memuatnya sesuai kebutuhan ketika TPdf pertama diaktifkan, sehingga DLL yang hilang baru muncul pada titik itu alih-alih pada saat aplikasinya dimulai. Jika Anda mengirimkan sebuah installer, pendekatan yang paling andal adalah menyalin DLL-nya ke folder aplikasinya saat pemasangan alih-alih bersandar pada direktori sistem yang kelak mungkin dibersihkan seorang administrator

Build V8 terutama berguna ketika Anda perlu berinteraksi dengan action JavaScript pada PDF, misalnya untuk memicu field kalkulasi atau handler submit. Sebuah viewer pembanding yang pasif tidak punya alasan menjalankan JavaScript; menyetel Pdf.FormFill := False sebelum Active := True melewatkan lingkungan pengisian form sepenuhnya, yang juga berarti tidak ada engine JS yang diinisialisasi bahkan jika build standarnya yang dipakai. Itulah default yang benar untuk sebuah viewer hanya-baca tanpa peduli varian DLL mana yang Anda kirimkan

Untuk detail lebih lanjut tentang PDFium Component dan API lengkapnya, kunjungi halaman produk Delphi PDFium Component