Artikel Teknis

Laporan PDF Delphi dengan HotPDF: TextOut, Font, dan Gambar

Menghasilkan sebuah report pada akhirnya adalah soal menempatkan tiga hal di sebuah halaman dan membuat ketiganya sepakat tentang di mana mereka berada: teks pada koordinat yang diketahui, font yang ter-render sama di server maupun di desktop Anda, dan gambar yang berukuran pas. Semua hal lain yang dilakukan sebuah library report tersusun di sekitar ketiga hal itu. HotPDF, library generasi PDF milik losLab untuk Delphi dan C++Builder, memberi Anda masing-masing sebagai sebuah pemanggilan langsung pada objek halaman, dan satu-satunya gesekan yang sesungguhnya adalah sistem koordinat di baliknya, yang berjalan ke arah yang berlawanan dari canvas VCL yang biasa Anda gunakan. Selesaikan orientasi itu lebih dulu dan sisa pekerjaan layout berhenti melawan Anda

Penempatan teks dan origin di kiri bawah

Hampir setiap report pertama semua orang muncul terbalik. Judulnya mendarat di dekat tepi bawah dan setiap baris di bawahnya justru naik ke arah atas. Tidak ada yang rusak. PDF user space, yang didefinisikan dalam ISO 32000-1 §8.3, menempatkan origin di sudut kiri bawah dengan Y bertambah ke atas, yang merupakan cerminan dari canvas GDI di mana Y bertambah ke bawah dari kiri atas. Lima menit yang dihabiskan untuk berdamai dengan itu menyelamatkan sebuah layout yang seharusnya Anda tulis ulang begitu angka-angkanya berhenti masuk akal

Diagram HotPDF yang mengontraskan titik asal koordinat kiri-atas VCL dengan asal kiri-bawah PDF, di mana TextOut menempatkan judul 50 poin dari atas halaman Letter di Y 792 dikurangi 50
PDF user space memantulkan canvas VCL, sehingga judul 50pt dari puncak halaman Letter adalah TextOut(50, 792 - 50, 0, 'INVOICE') dan konversi yang sama menjaga setiap koordinat laporan tetap intuitif

Pemanggilan utama objek halaman adalah TextOut(X, Y, Angle, Text). X dan Y menentukan lokasi teks dalam satuan point dari sudut kiri bawah, dan Angle memutarnya dalam derajat, dan itulah cara sebuah stempel DRAFT atau COPY diagonal digambar tanpa dukungan khusus apa pun. Trik yang membuat intuisi hasil latihan VCL tetap berfungsi adalah menyatakan Y sebagai tinggi halaman dikurangi jarak yang Anda inginkan dari atas:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt dari atas halaman Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // stempel yang diputar
    Pdf.AddPage;                                                // CurrentPage sekarang menunjuk ke sini
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // state font tidak terbawa
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dua perilaku stateful dalam listing itu bertanggung jawab atas sebagian besar bug yang hanya muncul di halaman kedua. AddPage mengarahkan ulang CurrentPage ke halaman yang baru saja dibuatnya, sehingga sebuah referensi halaman yang Anda simpan sebelumnya tidak lagi menggambar di tempat yang Anda harapkan. Pemilihan font juga bersifat per halaman, bukan per dokumen. Jika Anda melewatkan SetFont setelah sebuah AddPage, TextOut pertama pada halaman baru itu kembali ke default apa pun yang dimulai halaman tersebut, bukan font heading tebal yang Anda atur tiga halaman sebelumnya. Kebiasaan yang aman adalah memperlakukan "mulai halaman baru" dan "tegakkan kembali state teks" sebagai satu langkah yang tidak terpisahkan dalam loop report

Font yang ada di server, bukan hanya di desktop Anda

Sebagian besar masalah font sebenarnya adalah masalah deployment yang menyamar. Mesin pengembangan Anda memiliki font korporat yang terpasang, sehingga report itu terlihat benar di layar Anda dan dikirim. Host produksi menjalankan job itu di bawah sebuah service account yang belum pernah memasang font tersebut, renderer diam-diam menggantinya dengan sesuatu yang bisa ditemukannya, dan hal pertama yang terdengar oleh siapa pun adalah seorang pelanggan bertanya mengapa kop suratnya berubah. Jalan keluarnya adalah berhenti mempercayai direktori font OS dan memuat font dari sebuah file yang diletakkan installer Anda di disk. Pemanggilan registrasi Unicode milik HotPDF menerima sebuah path dan melakukan persis itu:

Diagram masalah penerapan font PDF Delphi: server produksi secara senyap mensubstitusi font yang hilang, sementara RegisterUnicodeTTF memuat TTF dari berkas yang diterapkan dan menyematkannya ke PDF
Bergantung pada direktori font OS patah ketika akun service produksi tak punya fontnya, sementara memuat TTF dari file yang di-deploy menyematkan glyph-nya dan setiap host merender identik
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));

TextOut menerima sebuah WideString langsung, dan itu lebih penting daripada yang terlihat sekilas. Sebuah nama pelanggan dengan aksen, sebuah jalan di Jerman, sebuah kota di Polandia: itu bukan kasus tepi, melainkan isi normal dari sebuah tabel pelanggan, dan semuanya melewati pemanggilan yang sama seperti label ASCII yang Anda hard-code, selama font yang terdaftar benar-benar memuat glyph-nya. Ada satu batasan versi yang menyertai font tertanam: dokumennya harus PDF 1.5 atau lebih baru, jadi jika sebuah persyaratan yang tidak terkait mengunci Anda pada versi yang lebih lama, itulah yang akan diam-diam rusak. Script kanan-ke-kiri seperti Arab dan Ibrani membutuhkan shaping yang sesungguhnya, bukan pencarian glyph yang lurus, dan itu punya pipeline-nya sendiri; lihat artikel kami tentang complex script text shaping dengan HotPDF

Ketika tidak ada font terpasang yang bisa mengekspresikan apa yang Anda butuhkan, bayangkan karakter MICR pada sebuah cek atau sebuah set simbol milik sendiri, font Type 3 mengisi celah itu. Anda mendefinisikan setiap glyph sebagai sebuah content stream kecil melalui RegisterType3Font dan AddType3Glyph. Ini adalah sudut API yang sangat spesifik dan Anda jarang akan membutuhkannya, namun jauh lebih rapi daripada menyebar ratusan bitmap simbol kecil di seluruh halaman

Gambar: argumen tengah adalah lebar dan tinggi, bukan sebuah sudut

Penanganan gambar terbagi menjadi dua langkah, dan menjaga keduanya tetap terpisah adalah inti dari semuanya. AddImage menerima sebuah TBitmap atau TJPEGImage, menanamkannya satu kali, lalu mengembalikan sebuah indeks. Artwork PNG harus di-decode menjadi sebuah bitmap sebelum sampai ke sana. ShowImage kemudian menggambar indeks itu di mana pun dan sesering apa pun yang Anda inginkan. Urutan argumen pada ShowImage adalah satu tempat yang layak diperlambat bacaannya:

Diagram pipeline gambar HotPDF di mana AddImage menyematkan bitmap sekali dan mengembalikan indeks, ShowImage menempatkannya berdasarkan lebar dan tinggi, dan urutan argumen bukan pasangan sudut
AddImage menyematkan piksel sekali dan setiap panggilan ShowImage memakai ulang indeks itu, dan argumen tengah adalah width dan height, bukan koordinat sudut yang berlawanan
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // men-decode PNG menjadi sebuah bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless untuk artwork warna datar
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): bukan (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Dua angka setelah posisi adalah lebar dan tinggi. Itu bukan koordinat sudut yang berlawanan, dan argumen terakhir adalah sudut rotasi dalam derajat. Baca signature-nya sebagai sebuah kotak X1/Y1/X2/Y2 dan sebuah logo berukuran 120-kali-40 yang ditempatkan pada (50, 700) justru terentang dari sana hingga (120, 40), melebar ke sebagian besar halaman. Outputnya membuat kesalahan itu terlihat jelas sementara kode sumbernya terlihat sepenuhnya masuk akal, dan itulah yang membuatnya menghabiskan satu sore penuh. KeepImageAspectRatio default-nya True, sehingga sebuah kotak dengan proporsi yang salah membuat letterbox pada gambar alih-alih mendistorsinya; balik ke False hanya ketika Anda memang bermaksud meregangkannya

Pemisahan antara meregistrasi dan menempatkan terbukti berharga pada run yang panjang. Karena AddImage menanamkan piksel satu kali dan setiap ShowImage dengan indeks itu menunjuk kembali ke objek tertanam yang sama, di mana Anda memanggil AddImage menentukan ukuran file. Panggil di dalam loop halaman untuk sebuah statement 500 halaman dan logo yang sama tertanam 500 kali. Panggil sekali sebelum loop, simpan indeksnya, dan logo itu tersimpan hanya satu kali. Sebuah dictionary kecil dengan kunci berupa path aset sudah cukup untuk memastikan setiap gambar yang berbeda hanya teregistrasi tepat satu kali

Pemilihan codec adalah tuas ukuran yang lain. Konten fotografis, lampiran hasil scan dan sejenisnya, tempatnya di JPEG: teruskan icJpeg ke AddImage dan turunkan JpegQuality ke sekitar 85, karena property itu dimulai dari 100 dan perbedaannya pada 85 tidak terlihat pada halaman cetak. Artwork warna datar seperti logo, chart, dan gambar garis tempatnya di icFlate, di mana kompresi lossless sudah kompak dan JPEG akan menimbulkan ringing yang terlihat di sekitar tepi yang tajam. Sebuah run statement yang mendorong satu foto kualitas penuh ke setiap halaman bisa membengkak menjadi berukuran gigabyte; konten yang sama pada JPEG 85 mendarat di sekitar sepersepuluh ukurannya, dan tidak ada reader yang bisa membedakannya

Garis, kotak, dan shading dengan primitif path

Garis horizontal di bawah header tabel dan kotak abu-abu di belakang angka total tidak perlu berupa gambar. Gambarlah sebagai vektor dan keduanya tetap tajam pada zoom berapa pun, tercetak dengan tajam, dan hampir tidak menambah apa pun pada ukuran file. HotPDF mengikuti model yang sama dengan yang digunakan content stream PDF mentah: bangun sebuah path, lalu panggil sebuah operator yang mewarnainya

// Garis horizontal di bawah header tabel
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Kotak total dengan shading: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

Urutannya tidak opsional: atur state warna, bangun path-nya, lalu panggil Stroke atau Fill. Sebuah path yang Anda bangun namun tidak pernah diwarnai tidak menyumbang apa pun pada halaman, dan itu hampir selalu menjadi jawaban ketika sebuah garis "tidak muncul." SetRGBFillColor menerima satu TColor, sehingga konstanta VCL yang sudah dikenal seperti clNavy dan clBlack langsung bisa dipakai, dan Rectangle menggunakan argumen lebar-dan-tinggi yang sama seperti penempatan gambar, bukan dua sudut. Satu peringatan tentang garis tipis: apa pun yang di bawah kira-kira setengah point bisa terlihat elegan di monitor lalu menghilang pada printer kantor 600 dpi, jadi 0,75pt adalah batas bawah yang masuk akal untuk garis mana pun yang harus tetap terlihat setelah dicetak

Paginasi terhadap data nyata, bukan data sampel

Satu detail yang harus benar sebelum layout-nya mengunci: kolom numerik harus rata pada tepi kanannya, dan cara melakukannya adalah mengukur lebar hasil render setiap nilai lalu memposisikannya mundur dari batas kolom, bukan mengisi string dengan spasi di depan. Padding dengan spasi hanya lurus dalam font monospace, dan tidak ada yang menyusun sebuah financial report dalam font monospace. Jalankan nilai-nilai itu lewat rutin yang sadar locale milik Delphi seperti FormatFloat terlebih dahulu, sehingga pemisah ribuan yang Anda ukur lebarnya adalah pemisah yang sama yang akan sungguh-sungguh ditampilkan locale pelanggan

Bahaya dengan paginasi adalah Anda menulisnya terhadap dataset demo, di mana sepuluh baris pendek muat dalam satu halaman dan loop-nya tidak pernah harus terputus. Produksi memberi Anda seorang pelanggan yang nama perusahaannya sepanjang 140 karakter dan sebuah statement dengan 4.000 baris item, dan sekarang loop itu harus terputus dengan benar setiap kali. Pola yang terbukti bertahan adalah satu kursor Y tunggal yang bergerak ke bawah seiring Anda mengurangi tinggi setiap baris, dan sebuah pemeriksaan yang memulai halaman baru pada saat kursor itu akan melewati margin bawah. Ke bawah di sini berarti Y berkurang, dan itulah satu-satunya tempat di mana origin kiri bawah tetap terasa kontra-intuitif. Simpan semua itu dalam satu rutin yang juga menerbitkan ulang SetFont dan menggambar ulang header berjalan pada halaman baru, dan bug off-by-one-page tidak akan pernah mendapat celah. Ketika report yang sama juga harus memenuhi aturan arsip atau aksesibilitas, pilihan yang Anda buat persis di sini, font mana yang Anda tanamkan, apakah outputnya tagged, ruang warna mana yang Anda gunakan, adalah hal-hal yang diawasi oleh standar-standar itu; panduan PDF/A, PDF/X, dan PDF/UA HotPDF layak dibaca sebelum template-nya mengeras

Setiap pemanggilan yang ditunjukkan di sini, penentuan posisi teks, registrasi font, penanaman gambar, dan penggambaran path, tersedia dalam HotPDF Delphi Component untuk Delphi dan C++Builder, yang referensinya mendokumentasikan seluruh output API berdampingan dengan fitur form, enkripsi, dan signing di sebelahnya