Technical Article

Merender Halaman PDF ke Monokrom 1-Bit di Delphi

Gateway faks tidak membutuhkan render halaman 24-bit Anda. Begitu juga pipeline arsip yang menyimpan sejuta faktur hasil pindai, atau front-end OCR yang membinerkan semuanya menjadi hitam putih sebelum ia sempat mencari karakter. Ketiganya menginginkan hal yang sama: bitmap 1-bit yang bersih, satu bit per piksel, dengan setiap titik hanya tinta atau kertas. Serahkan BMP berwarna penuh, dan mereka tetap akan membuang 23 bit per piksel, biasanya dengan dithering yang lebih buruk daripada yang bisa Anda lakukan sendiri. Pertanyaan yang menarik adalah di mana down-conversion itu seharusnya terjadi, dan jawaban di PDFlibPas ternyata memberi petunjuk berguna tentang cara memperluas renderer tanpa perlu menulis ulang

PDFlibPas adalah library PDF Object Pascal native untuk Delphi dan C++Builder. Core rendering-nya meraster halaman ke bitmap dan dapat menghasilkan BMP, PNG, JPEG, WMF, dan beberapa format lain. Yang belum bisa dilakukan sampai baru-baru ini adalah mengembalikan bitmap monokrom yang benar, atau merender hanya sebagian halaman. Keduanya hadir di v3.83.0, dan keduanya dibangun sebagai lapisan kenyamanan tipis di atas renderer yang sudah ada, bukan sebagai perubahan pada rasterizer itu sendiri. Batasan itu adalah inti ceritanya

Mengapa menurunkan ke 1-bit setelah render, bukan di dalam renderer

Cara yang paling jelas untuk menghasilkan gambar 1-bit adalah menyuruh rasterizer menggambar dalam 1-bit. Itu juga cara yang merusak semuanya yang lain. Bitmap internal renderer dibuat dengan PixelFormat := pf24bit yang di-hardcode di konstruktor PDFlibRenderer, dan permukaan 24-bit itu dipakai bersama oleh setiap jalur render: ekspor PNG, pratinjau device context, keluaran JPEG, semuanya. Jika Anda mengubahnya ke pf1bit dari sumbernya, Anda tidak menambah fitur monokrom, Anda menurunkan fidelitas warna untuk setiap pemanggil di library dan sekaligus menandatangani diri Anda untuk men-debug selusin regresi turunan

Jadi RenderPageToMonochromeFile menempuh jalan sebaliknya. Metode ini merender halaman seperti biasa, ke BMP sementara 24-bit, lalu baru mengubahnya menjadi 1-bit sebagai langkah pascaproses. Renderer tidak tersentuh. Perilaku monokrom hidup sepenuhnya di metode kenyamanan itu, jadi tidak mungkin memengaruhi siapa pun yang tidak memanggilnya. Inilah jenis trade-off yang layak disebut langsung: pascaproses menambah satu alokasi bitmap dan satu file sementara, dan sebagai gantinya ia menjaga core yang menopang seluruh sistem tetap di luar ruang lingkup. Untuk fitur yang ada demi melayani kasus faks dan arsip yang tepiannya sempit, itulah pilihan yang tepat

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI is the classic Group 4 fax resolution; page index is 1-based
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Bagaimana penurunan ke 1-bit benar-benar terjadi

Down-conversion ini mengandalkan GDI, bukan loop threshold buatan tangan, dan pilihan itu berpengaruh pada kualitas output. Di dalam metode, bitmap sementara 24-bit dimuat ke dalam TBitmap, TBitmap kedua dibuat dengan PixelFormat := pf1bit pada dimensi yang sama, lalu piksel dipindahkan dengan satu blit:

// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

Kuncinya ada pada SetStretchBltMode dengan HALFTONE. Walaupun sumber dan tujuan berukuran sama, jadi tidak ada penskalaan, mode stretch tetap menentukan bagaimana GDI memetakan warna ke palet 1-bit. HALFTONE membuatnya menerapkan dithering halftone, mengubah area abu-abu dan tepi teks antialias menjadi pola titik hitam dan putih alih-alih memotongnya keras ke warna terdekat dari dua warna. Jika pemanggilan mode itu dihilangkan, atau memakai default BLACKONWHITE, konten skala abu-abu akan berubah menjadi bentuk bertangga yang kotak-kotak. Untuk output dokumen hasil pindai dan praproses OCR, hasil dithering hampir selalu yang Anda inginkan

Satu detail tidak bisa ditawar dan gampang salah: render sementara harus berupa BMP. RenderPageToMonochromeFile memanggil renderer umum dengan kode opsi 0, yang berarti BMP. Argumen opsi pada RenderPageToFile adalah enum bilangan bulat kecil, dan nilainya tidak bisa saling dipertukarkan untuk tujuan ini: 0 adalah BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, dan seterusnya. Down-converter kemudian melakukan TBitmap.LoadFromStream pada file sementara. Jika Anda memberinya WMF dengan melewatkan 2, load itu akan melempar "Bitmap image is not valid", karena Windows Metafile adalah stream record vektor, bukan DIB. Down-conversion monokrom adalah operasi raster dari ujung ke ujung, jadi bentuk perantaranya harus format raster

Merender hanya sub-area halaman

Metode kedua, RenderPageRegionToFile, merender hanya sebuah persegi panjang di halaman, bukan seluruhnya. Kasus penggunaannya akrab begitu Anda pernah membangun document viewer apa pun: memotong blok tanda tangan dari kontrak, menghasilkan tile untuk peta besar yang diperbesar, atau mengambil satu area berstempel untuk thumbnail tanpa perlu meraster seluruh halaman pada DPI tinggi. Tanda tangannya sederhana:

// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

String clip terdiri dari empat double yang dipisahkan koma dalam titik PDF, lalu di-parse secara manual di dalam metode untuk menghindari keanehan locale dan DelimitedText. Dari lebar dan tinggi, metode menghitung ukuran bitmap keluaran sebagai Round(Width * DPI / 72) kali Round(Height * DPI / 72), mengalokasikan bitmap pf24bit di memori dengan ukuran persis itu, lalu merender ke device context-nya melalui RenderPageToDCClip. File hasilnya hanya berisi persegi panjang yang terpotong, dengan ukuran mengikuti region, bukan ukuran seluruh halaman

Parameter clip yang dulu tidak melakukan apa-apa

Di sinilah pekerjaannya ternyata lebih tajam daripada kelihatannya. RenderPageToDCClip sudah lama membawa parameter Clip, dan itu bohong. Pemanggilan menerima argumen itu, meneruskannya ke TPDFPageTree.RenderPageToDC, lalu implementasinya mengabaikannya sama sekali tanpa pernah menyerahkannya ke renderer. Anda bisa memberi persegi panjang apa pun dan tetap mendapatkan seluruh halaman. Siapa pun yang memasang RenderPageToDCClip dengan harapan crop akan menerima render penuh halaman dan, tergantung layout mereka, mungkin bahkan tidak menyadarinya

v3.83.0 menyambungkan kabelnya. RenderPageToDC sekarang mem-parse persegi panjang titik "Left,Top,Width,Height" yang sama dan menerapkannya sebagai region clip GDI nyata pada device context tujuan sebelum renderer menggambar. Konversi dari point ke piksel device memakai faktor skala biasa DPI / 72, diterapkan ke keempat sisi. Urutan di sekitar render adalah pola save, clip, restore yang standar:

// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);

Pasangan SaveDC / RestoreDC(-1) inilah yang membuatnya aman dipanggil berulang kali: region clip didorong ke stack state DC, halaman digambar, lalu clip asli dipulihkan lagi apa pun yang terjadi saat render berakhir. RestoreDC(TargetDC, -1) memulihkan state yang paling baru disimpan, yang merupakan idiom standar untuk save/restore yang seimbang. Jika restore dilewatkan, pemanggil yang memakai DC yang sama untuk render halaman penuh berikutnya akan mendapati hasilnya terpotong secara misterius ke region terakhir. Memperbaiki parameter mati itu juga memperbaiki RenderPageRegionToFile secara cuma-cuma, karena metode baru itu lewat tepat melalui jalur ini

Satu poin perilaku yang perlu diingat: clip itu memotong, bukan menskalakan. Halaman tetap diraster pada DPI yang Anda minta, pada posisi normalnya, dan region clip hanya membuang semua yang berada di luar persegi panjang. Anda tidak memperbesar region agar mengisi output, Anda memotong jendela dari render resolusi penuh. Jika Anda ingin region diperbesar, naikkan DPI. Koordinat persegi panjang ditafsirkan di ruang device setelah penskalaan point ke piksel, diukur dari sudut kiri atas permukaan yang dirender, jadi rencanakan Left dan Top dari bagian atas halaman ke bawah. Untuk tur yang lebih dalam tentang bagaimana PDFlibPas menggerakkan device context untuk output di layar, artikel pendamping tentang pratinjau cetak dan output device context membahas plumbing DC yang sama dari sisi tampilan

Batas yang jujur: BMP 1-bit, bukan TIFF G4

Akan mudah untuk melebih-lebihkan ini sebagai output siap-faks, jadi berikut batasannya secara lugas. RenderPageToMonochromeFile menghasilkan BMP pf1bit. Ia tidak menghasilkan TIFF CCITT Group 4, yang biasanya diharapkan oleh workflow faks sungguhan atau arsip TIFF. Alasannya konkret, bukan karena kelalaian: unit CCITT PDFlibPas saat ini memang mendekode stream G4, tetapi belum punya encoder G4. Tanpa encoder, tidak ada tempat untuk menulis run monokrom terkompresi, jadi jalur monokrom berhenti di DIB 1-bit yang tidak terkompresi

Dalam praktiknya itu tetap berguna. BMP 1-bit adalah format piksel yang tepat, sudah didithering dan siap dipakai, dan sebagian besar toolchain faks, arsip, atau OCR akan dengan senang hati menerimanya atau mengonversinya ke G4 sendiri dengan satu langkah lanjutan. Namun jika kebutuhan Anda secara harfiah adalah TIFF Group 4 langsung dari library, itu belum tersedia, dan Anda perlu menyiapkan tahap kompresi sendiri. Mengetahui di mana sebuah fitur berhenti sama pentingnya dengan mengetahui apa yang dilakukannya

Kedua metode ini sengaja dibuat kecil, dan itulah pelajaran desain yang layak dibawa dari halaman ini: API kenyamanan yang duduk di atas renderer bisa menambah kemampuan nyata, keluaran monokrom, pemotongan region, tanpa menyentuh rasterizer dan mengguncang pemanggil lain. Saat Anda memang perlu memilih di antara mesin render untuk rasterisasi dasarnya, ringkasan tentang rendering PDF multi-engine di Delphi membahas trade-off-nya secara mendalam. Untuk melihat seluruh permukaan rendering dan sisa API, halaman produk PDFlibPas Delphi PDF Library menampilkan gambaran lengkap