Artikel Teknis

Membuat PDF dari Nol dengan PDFium Component di Delphi

PDFium memiliki reputasi sebagai viewer engine, penyaji di balik tab PDF Chrome, jadi hal pertama yang perlu dijelaskan adalah bahwa PDFium Component juga dapat membangun dokumen yang belum pernah ada sebelumnya. Sisi penyusunan membungkus API objek halaman milik PDFium: Anda membuat dokumen kosong, menambahkan halaman dengan dimensi eksplisit, dan meletakkan teks, jalur vektor, dan gambar ke setiap halaman pada koordinat yang Anda pilih. Tidak ada bahasa deskripsi halaman yang perlu dipelajari dan tidak ada printer driver dalam alurnya. Anda memanggil metode, library menyusun objek PDF, dan SaveAs menserialisasi hasilnya

Apa yang tidak Anda dapatkan adalah layout engine. Hal ini cukup penting untuk dinyatakan di awal, karena ini membentuk setiap contoh di bawah ini. PDFium Component menempatkan konten di tempat yang Anda tentukan, dalam koordinat absolut, dan tidak di tempat lain. Ini tidak akan membungkus paragraf secara otomatis, mengalirkan teks melintasi pemisah halaman, atau menghitung tabel dari baris dan kolom. Itu adalah tugas Anda. Jika Anda datang mengharapkan sesuatu yang mengalirkan kembali prosa seperti yang dilakukan pengolah kata, kalibrasi sekarang: ini adalah API penempatan tingkat rendah yang presisi, lebih dekat dengan menggambar di kanvas daripada menyusun dokumen. Untuk faktur, sertifikat, label, dan halaman laporan yang dihasilkan di mana Anda sudah tahu di mana setiap elemen berada, presisi itu persis seperti yang Anda inginkan

PDFium Component bukan layout engine. Ia menempatkan konten pada koordinat absolut yang Anda berikan dan tidak otomatis membungkus paragraf, mengalirkan teks ke halaman berikutnya, atau menghitung tabel. Ketelitian ini cocok untuk invoice, sertifikat, label, dan laporan yang tata letaknya sudah diketahui

Persyaratan minimum untuk menghasilkan file

Tiga panggilan berada di antara objek TPdf yang kosong dan PDF yang disimpan: buat dokumen, tambahkan halaman, tulis ke disk. Hal lainnya adalah konten yang Anda susun di antaranya

uses
  Vcl.Graphics,   // untuk clBlack dan TColor
  PDFium;         // tempat TPdf berada

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // dokumen kosong di memori
    Pdf.AddPage(0, 595, 842);           // A4 portrait, dalam poin
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialisasikan ke disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Satu detail menjebak orang-orang yang telah melihat potongan kode lama: Anda tidak menetapkan Pdf.Active := True setelah CreateDocument. Properti Active melaporkan apakah handel dokumen ada, dan CreateDocument telah membuatnya, sehingga properti tersebut bernilai True saat panggilan tersebut kembali. Menetapkannya kembali tidak berguna dan menyesatkan pembaca berikutnya. Properti Active berguna saat penutupan: menetapkan nilai False akan melepaskan dokumen dasar sebelum Free, yang merupakan urutan pembersihan yang bersih. Perlakukan CreateDocument dan pembukaan file sebagai hal yang saling eksklusif. Library menolak membuat dokumen baru pada TPdf yang sudah membuka dokumen, jadi penggunaan kembali berarti menutup dokumen saat ini terlebih dahulu

Koordinat dimulai dari sudut kiri bawah

Pasangan argumen kedua untuk AddText, dan untuk setiap panggilan penempatan, adalah poin dalam ruang pengguna PDF. Asal koordinat berada di sudut kiri bawah halaman, X berjalan ke kanan, dan Y berjalan ke atas. Satu unit adalah satu poin, 1/72 inci, jadi halaman A4 adalah 595 x 842 unit dan US Letter adalah 612 x 792. Y yang mengarah ke atas adalah satu-satunya sumber kebingungan "teks saya berada di luar halaman", karena koordinat layar dan bitmap meletakkan titik asal di atas dengan Y tumbuh ke bawah. Pada halaman setinggi 842 poin, tajuk di dekat bagian atas berada di sekitar Y 780, bukan Y 60. Ketika sebuah tulisan mendarat di tempat yang tidak terduga, tinggi halaman dikurangi Y Anda hampir selalu merupakan angka yang sebenarnya Anda maksudkan

AddPage mengambil posisi penyisipan sebagai argumen pertamanya, berbasis satu, dengan 0 sebagai singkatan 'awal dokumen' yang nyaman. Berikan nilai 0 atau 1 untuk halaman pertama dan halaman tersebut dimasukkan di depan; berikan nilai yang cocok dengan jumlah halaman untuk menambahkan di bagian akhir. Halaman yang baru ditambahkan juga menjadi halaman saat ini, tujuan panggilan gambar berikutnya, sehingga tidak ada langkah terpisah untuk memilih halaman ini setelah menambahkannya. Jika Anda menambahkan beberapa halaman dan kemudian perlu menggambar kembali ke halaman sebelumnya, atur properti PageNumber untuk memindahkan kursor; jika Anda mengisi halaman secara berurutan saat membuatnya, Anda dapat membiarkannya saja

Menulis teks, dan aturan font yang menjebak secara diam-diam

Tanda tangan AddText membawa semua yang dibutuhkan oleh sebuah teks: string, nama font, ukuran dalam poin, jangkar X dan Y, lalu warna opsional, byte alfa untuk transparansi, dan sudut rotasi dalam derajat

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Judul dalam warna hitam, opasitas default, tanpa rotasi
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // Garis byline yang lebih terang 24 poin di bawahnya
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // Stempel draft samar secara diagonal di seluruh halaman
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Byte alfa berjalan dari $00 (tidak terlihat) hingga $FF (buram), yang membuat stempel draft menjadi watermark daripada blok padat: $30 secara kasar adalah sembilan belas persen opasitas, cukup untuk dibaca. Sudut memutar teks berlawanan arah jarum jam di sekitar jangkarnya, jadi 45 derajat memberikan stempel sudut-ke-sudut klasik. Semua ini tidak memerlukan fitur tanda air terpisah. Tanda air hanyalah panggilan AddText yang besar, semi-transparan, dan diputar, dan menggambarnya sebelum atau sesudah badan menentukan apakah ia berada di belakang atau di atas konten

Font layak mendapatkan perhatian khusus, karena mode kegagalannya berjalan diam-diam. Saat Anda memberikan nama font, PDFium Component meminta sistem operasi untuk data TrueType font tersebut dan menyematkannya dalam dokumen, itulah sebabnya file yang dibuat di mesin Anda merender secara identik di mesin yang belum pernah memasang font tersebut. Masalahnya adalah apa yang terjadi ketika nama tersebut tidak ditemukan: salah ketik, atau font tidak ada di mesin kompilasi. Tidak ada eksepsi yang dilemparkan. Library kembali membuat objek teks yang membawa nama tersebut sebagai label saja, tanpa ada yang disematkan, dan membiarkan viewer untuk mengganti apa pun yang dianggapnya dekat. Teks muncul dalam pengujian Anda, terlihat masuk akal, dan mengubah metrik atau glif saat file dibuka di tempat yang memiliki font berbeda. Gunakan nama yang Anda ketahui ada di mesin pembuat, perlakukan daftar font sebagai dependensi penyebaran, dan buka sampel di viewer pada sistem yang bersih sebelum Anda mempercayai hasilnya

Bentuk vektor: membuat path, lalu berkomitmen

Garis, persegi panjang, dan wilayah yang diisi melewati path. Anda membukanya dengan CreatePath, yang menetapkan titik awal dan semua gaya sekaligus, mode pengisian, warna pengisian dan garis tepi dengan byte alfa mereka sendiri, serta lebar garis. Kemudian Anda memperluasnya dengan LineTo, BezierTo, dan ClosePath, dan akhirnya AddPath melakukan komit pada path yang telah selesai ke halaman. Langkah komit mudah dilupakan dan tidak menghasilkan apa-apa jika dilewati

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // Aturan horizontal tipis. Overload persegi panjang menetapkan kotak secara langsung:
  // X, Y, Lebar, Tinggi, lalu mode pengisian dan warna.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Titik awal pada vertex pertama, garis ke sisa, tutup.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // tidak ada yang digambar sampai ini dijalankan
end;

Dua overload mencakup kasus-kasus umum. Bentuk empat koordinat mengambil X, Y, lebar, dan tinggi dan memberi Anda persegi panjang dalam satu panggilan, yang dapat digunakan untuk menggambar garis pembatas, batas sel, atau panel latar belakang yang diisi. Bentuk dua koordinat hanya menetapkan titik awal, dan Anda melacak sisa garis besar sendiri dengan LineTo dan BezierTo. Mode pengisian mengontrol bagaimana wilayah yang tumpang tindih dicat: fmWinding cocok untuk sebagian besar bentuk padat, fmAlternate menangani potongan dan garis besar yang berpotongan sendiri, dan fmNone menyisakan path stroked-only tanpa pengisian, yang digunakan oleh pembagi di atas

Tabel adalah path dan teks, disusun secara manual

Karena tidak ada tabel primitif, tabel adalah perulangan. Anda memutuskan offset X kolom dan tinggi baris, menulis setiap sel dengan AddText, dan menggambar garis pembatas dengan path persegi panjang. Aritmatikanya adalah tugas Anda, tetapi itu sederhana, dan setelah ditulis dapat digeneralisasikan ke grid apa pun yang Anda butuhkan

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // kolom offset
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Baris header
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Garis di bawah header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Baris data, melangkah Y ke bawah setiap iterasi
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Perhatikan Y melangkah ke bawah sebesar tinggi baris setiap lintasan, sekali lagi karena arah atas adalah nilai positif. Ini juga merupakan tempat di mana ketiadaan pengukuran teks terlihat: tidak ada yang menghentikan nama item yang panjang dari menimpa ke kolom berikutnya, karena library tidak tahu seberapa lebar string Anda dirender. Untuk output format tetap di mana Anda mengontrol data, Anda mengukur kolom secara murah hati dan melanjutkan. Untuk konten yang sangat bervariasi, Anda membatasi masukan atau mengukur lebar glif sendiri sebelum menempatkannya, yang merupakan titik di mana library komposisi khusus mulai memberikan manfaat

Gambar dan halaman ganda

Konten raster masuk melalui pembantu gambar. AddPicture mengambil TPicture yang dimuat dan menempatkannya pada satu titik, dengan lebar dan tinggi opsional untuk menskalakannya; AddImage menerima jalur file atau TBitmap secara langsung, dan AddJpegImage mengalirkan byte JPEG tanpa konversi melalui bitmap. Seperti hal lainnya, koordinat penempatan adalah sudut kiri bawah gambar di ruang pengguna, dan lebar serta tinggi adalah ukuran pada halaman dalam poin, bukan dimensi piksel dari sumber

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; halaman baru menjadi halaman saat ini
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer dekat tepi bawah
      // ... gambar isi halaman ini di sini ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Dokumen multi-halaman adalah pola halaman tunggal dalam sebuah perulangan. Setiap AddPage menyisipkan halaman dan membuatnya aktif, sehingga badan dan footer yang Anda gambar berikutnya mendarat di halaman yang baru saja Anda tambahkan. Anda tidak menetapkan ulang PageNumber di dalam perulangan ini, karena menambahkan halaman sudah memindahkan kursor ke sana; Anda hanya memerlukan PageNumber saat kembali ke halaman di luar urutan pembuatan. Panggil SaveAs sekali di akhir, setelah halaman terakhir diisi. Jika Anda membutuhkan profil arsip daripada file biasa, objek dokumen yang sama mengekspos SaveAsPdfA dan varian kesesuaian lainnya, sehingga pilihan standar output adalah panggilan penyimpanan yang berbeda, bukan jalur pembuatan yang berbeda

Di mana ini cocok

Penjelasan jujurnya adalah bahwa API pembuatan milik PDFium Component adalah lapisan tipis yang setia atas model objek halaman PDFium: pembuatan dokumen nyata, font tertanam yang nyata, konten vektor dan raster yang nyata, diserialisasikan ke file yang sesuai standar. Ini bukan, dan tidak berpura-pura menjadi, reflowing document engine. Garis pembatasnya adalah tata letak teks. Jika output Anda berupa faktur bertabulasi, sertifikat, label, dashboard yang dirender ke grid tetap, model koordinat absolut adalah pilihan langsung dan cepat serta kode tetap dapat dibaca. Jika output Anda berupa prosa bentuk panjang yang harus membungkus dan membagi halaman dengan sendirinya, Anda akan membangun kembali mesin tata letak di atas panggilan ini, dan itu adalah alat yang salah untuk pekerjaan tersebut. Mengetahui di sisi mana garis pembatas tersebut Anda berada adalah bagian terbesar dari keputusan

Metode pembuatan yang dijelaskan di sini adalah bagian dari PDFium Component untuk Delphi, yang memadukan jalur pembuatan ini dengan fitur penyajian dan ekstraksi teks yang lebih dikenal dari PDFium