Artikel Teknis

Mencetak Dokumen PDF dengan PDFium Component di Delphi

Koordinat PDF dalam poin, koordinat printer dalam satuan perangkat, dan keduanya tidak ada hubungannya sampai Anda mengonversinya secara sengaja. Ketidakcocokan itu adalah akar dari sebagian besar output cetak yang buruk dalam aplikasi Delphi: kode mengirim file yang benar tetapi halaman keluar terpotong, meregang, atau kosong. PDFium Component menangani sisi rendering dengan bersih; plumbing printer adalah VCL standar. Keduanya cocok bersama dengan sejumlah kode yang modis setelah Anda memahami apa yang diharapkan masing-masing sisi

Cara kerja pipeline render-lalu-cetak

PDFium Component tidak berbicara langsung ke printer. Polanya adalah: render halaman ke TBitmap pada resolusi yang Anda inginkan, lalu transfer bitmap tersebut ke kanvas printer dengan StretchDIBits. TPdf.RenderPage mengembalikan bitmap yang dimiliki pemanggil, sehingga Anda mengontrol dimensi piksel. Lewati [rePrinting] dalam set opsi dan PDFium mengalihkan jalur renderingnya ke jalur yang menghilangkan efek khusus layar seperti petunjuk subpiksel LCD, dan menangani MediaBox halaman dengan benar untuk output cetak. Biarkan rePrinting dan apa yang Anda kirim ke printer adalah render layar, yang terlihat baik-baik saja di monitor tetapi cenderung menghasilkan output yang lebih lembut pada printer DPI tinggi karena keputusan petunjuk yang dibuat untuk layar 96 DPI tidak cocok untuk pencetakan 300 atau 600 DPI

TPdf.Active adalah satu-satunya gerbang yang perlu diperiksa sebelum menyentuh properti halaman apa pun. Komponen menelan kesalahan pemuatan secara diam-diam: menetapkan Active := True pada file yang rusak atau dilindungi kata sandi tidak memunculkan pengecualian; itu hanya membiarkan Active sebagai False. Selalu periksa setelah penugasan. Membaca PageCount atau PageWidth pada dokumen yang tidak aktif mengembalikan nol, yang menghasilkan no-op diam yang sangat sulit didiagnosis setelah mencapai spooler

Loop cetak minimal

Kasus kerja paling sederhana memuat file, membuka pekerjaan cetak, mengiterasi halaman, dan menutup. Satu-satunya detail rumit adalah bahwa Printer.NewPage tidak boleh dipanggil sebelum halaman pertama, karenanya flag FirstPage. Transfer StretchDIBits melewati GetDIBSizes dan GetDIB untuk menarik bit device-independent dari handle bitmap, lalu melukisnya ke kanvas printer pada ukuran halaman penuh:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // load failed silently; bail out

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // Render at printer resolution; rePrinting adjusts the render path
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Melewati Printer.PageWidth dan Printer.PageHeight sebagai dimensi bitmap berarti Anda merender pada ukuran piksel asli printer, yang sudah memperhitungkan DPI perangkat. Panggilan StretchDIBits kemudian memetakan piksel tersebut 1:1 ke halaman. Ini memberi Anda kesetiaan terbaik yang dapat dicapai tanpa aritmatika DPI eksplisit, tetapi hanya berfungsi ketika halaman PDF dan kertas fisik kebetulan berukuran sama. Ketika berbeda, Anda memerlukan penskalaan eksplisit

Penskalaan ketika ukuran halaman dan kertas berbeda

Halaman PDF pada potret A4 tidak secara otomatis sesuai printer US Letter, dan halaman lanskap yang dimasukkan ke printer berorientasi potret akan terpotong. Pendekatan standar adalah menghitung faktor skala seragam dari rasio piksel printer terhadap poin PDF, lalu menerapkannya ke kedua dimensi sehingga rasio aspek dipertahankan. Pdf.PageWidth dan Pdf.PageHeight mengekspos dimensi halaman saat ini dalam poin, di mana satu poin adalah 1/72 inci. Mengalikan dengan DPI target dan membagi dengan 72 mengonversi ke piksel pada resolusi tersebut. Ambil Min dari rasio X dan Y untuk mendapatkan skala terbesar yang masih pas dalam area yang dapat dicetak:

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transfer with StretchDIBits as above
end;

Merender pada Dpi = 300 cocok untuk sebagian besar printer kantor. Pada 600 DPI, bitmap untuk satu halaman A4 mencapai sekitar 34 megapiksel, yang sekitar 100 MB sebagai bitmap 32-bit; peningkatan kualitas untuk dokumen teks biasa minimal dan biaya memori per halaman signifikan. Simpan 600 DPI untuk toko cetak atau gambar teknis berat vektor di mana itu benar-benar penting

Flag reAnnotations dalam blok kode kedua independen dari rePrinting. Sertakan ketika pengguna mengharapkan cap, sorotan, dan kotak komentar muncul di kertas. Hilangkan untuk output khusus konten. Kedua flag dapat digabungkan secara bebas

Rotasi halaman

PDFium menyimpan rotasi halaman dalam PDF sebagai entri /Rotate, dapat diakses melalui Pdf.PageRotation, yang mengembalikan nilai TRotation (ro0, ro90, ro180, ro270). Sistem koordinat printer membalik rotasi 90 dan 270 derajat relatif terhadap layar. Jika Anda melewati nilai PageRotation mentah langsung ke RenderPage tanpa penyesuaian apa pun, halaman lanskap yang tertanam dalam dokumen potret akan dicetak terbalik pada sebagian besar driver printer Windows. Perbaikannya adalah pertukaran sederhana sebelum panggilan render: petakan ro90 ke ro270 dan ro270 kembali ke ro90, biarkan ro0 dan ro180 tidak berubah

Verifikasi perilaku ini pada printer target spesifik Anda sebelum pengiriman. Perilaku driver seputar rotasi tidak seragam di seluruh vendor, dan beberapa driver menerapkan koreksi rotasi mereka sendiri di level GDI. Jika Anda melihat rotasi ganda, hapus pertukaran; jika Anda tidak melihat koreksi sama sekali, tambahkan. Dokumen campuran orientasi dengan halaman potret dan lanskap bergantian adalah cara tercepat untuk menangkap salah satu mode kegagalan selama pengujian

Manajemen memori di sepanjang pekerjaan cetak panjang

Setiap panggilan ke RenderPage mengalokasikan TBitmap baru yang dimiliki pemanggil dan harus dibebaskan. Dalam loop di atas, blok try/finally Bitmap.Free menangani ini dengan benar untuk satu halaman pada satu waktu. Jangan akumulasikan bitmap di seluruh halaman: render 300 DPI dari dokumen 200 halaman akan menghabiskan gigabyte sebelum halaman pertama mencapai spooler. Bebaskan setiap bitmap sebelum melanjutkan ke halaman berikutnya

Pasangan AllocMem / FreeMem di dalam blok transfer mengikuti aturan yang sama. GetDIBSizes memberi tahu Anda berapa banyak memori yang dibutuhkan header DIB dan data piksel; Anda mengalokasikan, mengisi, melukis, dan membebaskan semuanya dalam lingkup satu halaman. Membiarkan salah satu blok bocor akan menyebabkan pekerjaan cetak menguras heap proses pada dokumen yang lebih panjang dari beberapa lusin halaman

Jika Anda perlu menjalankan pekerjaan cetak pada thread latar belakang, simpan TPdf dan semua panggilan printer VCL di thread yang sama. TPdf itu sendiri tidak aman thread di seluruh instance yang berbagi status global DLL PDFium; model paling aman adalah satu TPdf per thread, masing-masing memuat salinan filenya sendiri

API rendering dan dokumen yang ditunjukkan di sini adalah bagian dari PDFium Component untuk Delphi dan C++Builder