Teknik Makale

Delphi'de PDF Sayfalarını 1-Bit Monokroma Render Etme

Bir faks geçidi 24-bit sayfa render'ınızı istemez. Milyonlarca taranmış görünümlü faturayı saklayan arşiv hattı da istemez; karakter aramaya başlamadan önce her şeyi siyah-beyaza eşikleyen OCR ön ucu da istemez. Üçünün de istediği aynı şeydir: piksel başına bir bit taşıyan temiz 1-bit bitmap; her nokta ya mürekkep ya da kâğıttır. Bunlara tam renkli BMP verirseniz piksel başına 23 biti zaten atarlar; üstelik çoğu zaman sizin kendiniz yapabileceğinizden daha kötü bir dithering geçişiyle. İlginç soru, bu aşağı dönüştürmenin nerede yapılması gerektiğidir; PDFlibPas içindeki cevap da, yeniden yazmayı istemediğiniz bir renderer'ı genişletme konusunda faydalı bir şey söyler

PDFlibPas, Delphi ve C++Builder için yerel bir Object Pascal PDF kütüphanesidir. Rendering çekirdeği sayfayı bitmap'e rasterize eder ve BMP, PNG, JPEG, WMF ve birkaç başka biçime çıktı verebilir. Yakın zamana kadar yapamadığı şey, gerçek bir monokrom bitmap geri döndürmek ya da sayfanın yalnızca bir bölümünü render etmekti. Her ikisi de v3.83.0'da geldi ve ikisi de rasterizer'ın kendisindeki değişiklikler olarak değil, mevcut renderer üstüne oturan ince convenience katmanları olarak inşa edildi. Hikâyenin bütünü bu kısıtta yatıyor

Aşağı dönüştürme neden renderer içinde değil, rendering sonrasında yapılır

1-bit görsel üretmenin bariz yolu, rasterizer'a 1-bit çizmesini söylemektir. Bu aynı zamanda diğer her şeyi bozan yoldur. Renderer'ın iç bitmap'i PixelFormat := pf24bit ile PDFlibRenderer constructor'ı içinde sabit olarak oluşturulur ve bu 24-bit yüzey bütün render yollarınca paylaşılır: PNG dışa aktarımı, device-context preview, JPEG çıktısı; hepsi. Bunu kaynakta pf1bit olarak değiştirirseniz monokrom özellik eklemiş olmazsınız; kütüphanedeki her çağıran için renk sadakatini düşürür ve aşağı akışta bir düzine gerilemeyi ayıklama işini üstlenirsiniz

Bu yüzden RenderPageToMonochromeFile tam ters yolu izler. Sayfayı önce normal şekilde, geçici 24-bit BMP'ye render eder ve ancak ondan sonra 1-bit'e çökertir. Renderer'a hiç dokunulmaz. Monokrom davranış bütünüyle convenience yönteminde yaşar; dolayısıyla onu çağırmayan hiç kimse etkilenmez. Bu tür ödünleşimi açıkça adlandırmaya değer: sonradan işleme bir ek bitmap tahsisi ve geçici dosya maliyeti öder, karşılığında yük taşıyan çekirdeği tamamen kapsam dışında tutar. Faks ve arşiv gibi kenar durumlara hizmet eden özellik için bu, muhasebe defterinin doğru tarafıdır

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;

1-bit çökertme gerçekte nasıl olur

Aşağı dönüştürme, el yazımı eşik döngüsü yerine GDI'ye yaslanır ve bu seçim çıktı kalitesi açısından önemlidir. Yöntemin içinde 24-bit geçici bitmap bir TBitmap içine yüklenir, aynı boyutlarda TBitmap ile ikinci bir PixelFormat := pf1bit oluşturulur ve pikseller tek blit ile taşınır:

// 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');

İşin püf noktası SetStretchBltMode ile HALFTONE seçimidir. Kaynak ve hedef aynı boyutta olduğu için gerçek ölçekleme yapılmasa bile stretch mode, GDI'nin renkleri 1-bit palette'e nasıl eşleyeceğini yine belirler. HALFTONE, gri bölgeler ve antialias uygulanmış metin kenarlarını iki renkten en yakınına sertçe kesmek yerine siyah-beyaz nokta desenlerine çevirecek halftone dithering uygulamasını sağlar. Mode çağrısını düşürürseniz veya varsayılan BLACKONWHITE kullanırsanız, gri tonlamalı içerik bloklu, posterize eşiklenmiş şekillere dönüşür. Taratılmış belge ve OCR ön işleme çıktısı için dither edilmiş sonuç neredeyse her zaman istediğiniz şeydir

Müzakereye kapalı ve yanlış yapması kolay bir ayrıntı daha vardır: geçici render mutlaka BMP olmalıdır. RenderPageToMonochromeFile, genel renderer'ı 0 seçenek koduyla çağırır; bu, BMP'dir. RenderPageToFile üzerindeki options bağımsız değişkeni küçük tamsayı enum'udur ve bu amaç için değerler birbirinin yerine geçmez: 0 BMP'dir, 1 JPEG'dir, 2 WMF'dir, 3 EMF'dir, 5 PNG'dir ve böyle sürer. Aşağı dönüştürücü sonra geçici dosya üzerinde TBitmap.LoadFromStream yapar. 2 geçirerek ona WMF verirseniz yükleme "Bitmap image is not valid" hatasını fırlatır; çünkü Windows Metafile vektör kayıt akışıdır, DIB değildir. Monokrom aşağı dönüştürme baştan sona raster işlemidir; dolayısıyla ara format da raster olmak zorundadır

Sayfanın yalnızca alt bölgesini render etme

İkinci yöntem olan RenderPageRegionToFile, tüm sayfa yerine sayfanın yalnızca bir dikdörtgenini render eder. Herhangi bir belge görüntüleyici kurduysanız kullanım durumları tanıdıktır: sözleşmeden imza bloğunu kırpmak, büyük çizimin yakınlaştırılmış haritası için karo üretmek ya da yüksek DPI ile tüm sayfayı rasterize etme maliyetine girmeden tek damgalı bölgeyi küçük görsel olarak çekmek. İmza basittir:

// 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');

Clip dizgesi, locale ve DelimitedText tuhaflıklarını aşmak için yöntemin içinde elle ayrıştırılan, virgülle ayrılmış dört double'dan oluşur. Genişlik ve yükseklikten yöntem çıktı bitmap boyutunu Round(Width * DPI / 72) ile Round(Height * DPI / 72) olarak hesaplar, tam bu boyutta bellekte pf24bit bitmap ayırır ve bunun device context'ine RenderPageToDCClip üzerinden render eder. Sonuç dosyası, tam sayfa yerine yalnızca kırpılmış dikdörtgeni içerir ve tüm sayfaya değil bu bölgeye göre boyutlanır

Hiçbir şey yapmayan clip parametresi

İşin, göründüğünden daha keskin olduğu nokta burasıdır. RenderPageToDCClip uzun süredir Clip parametresi taşıyordu ve bu bir yalandı. Çağrı bağımsız değişkeni kabul ediyor, bunu TPDFPageTree.RenderPageToDC içine iletiyor ve uygulama bunu bütünüyle yok sayıyordu; renderer'a hiç vermiyordu. İstediğiniz dikdörtgeni geçebiliyor ve yine tam sayfayı geri alıyordunuz. Kırpma bekleyerek RenderPageToDCClip bağlamış olan herkes tam sayfa render alıyordu; yerleşimine bağlı olarak bunu fark etmemiş bile olabilir

v3.83.0 bu teli gerçekten bağladı. RenderPageToDC artık aynı "Left,Top,Width,Height" point dikdörtgenini ayrıştırıyor ve renderer çizmeden önce bunu hedef device context üzerinde gerçek GDI clip region olarak uyguluyor. Point'ten cihaz pikseline dönüşüm, her dört kenara da uygulanan alışılmış DPI / 72 ölçek katsayısıdır. Render çevresindeki sıra, standart save/clip/restore dansıdır:

// 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);

Buradaki SaveDC / RestoreDC(-1) çifti, bunun tekrar tekrar güvenle çağrılmasını sağlayan şeydir: clip region DC durum yığınına itilir, sayfa çizilir ve orijinal clip, render nasıl çıkarsa çıksın geri alınır. RestoreDC(TargetDC, -1), en son kaydedilmiş durumu geri yükler; dengeli save/restore için standart deyim budur. Restore'u atlarsanız, aynı DC'yi daha sonraki tam sayfa render için yeniden kullanan çağıran, gizemli biçimde son bölgeye kırpılmış çıktıyla karşılaşır. Ölü parametreyi düzeltmek RenderPageRegionToFile yöntemini de bedavaya düzeltti; çünkü yeni yöntem tam olarak bu yol üzerinden gider

İçselleştirilmesi gereken davranış noktası şudur: clip kırpar, ölçeklemez. Sayfa hâlâ istediğiniz DPI ile normal konumunda rasterize edilir; clip region yalnızca dikdörtgen dışındaki her şeyi atar. Bölgeyi çıktıyı dolduracak şekilde yakınlaştırmıyorsunuz; tam çözünürlüklü render'dan pencere kesiyorsunuz. Büyütülmüş bölge istiyorsanız DPI'ı yükseltin. Dikdörtgen koordinatları, point'ten piksele ölçekleme sonrasında cihaz uzayında yorumlanır; render yüzeyinin sol üst köşesinden ölçülür. Bu yüzden Left ve Top değerlerini sayfanın tepesinden aşağıya doğru planlayın. PDFlibPas'ın device context'i ekrandaki çıktı için nasıl sürdüğüne dair daha derin tur isterseniz, yazdırma önizleme ve device-context çıktısı üzerine eşlikçi yazı aynı DC tesisatını görüntü tarafında anlatır

Dürüst sınır: 1-bit BMP, G4 TIFF değil

Bunu "faks hazır çıktı" diye abartmak kolay olurdu; bu yüzden sınırı açıkça koyalım. RenderPageToMonochromeFile, bir pf1bit BMP üretir. Gerçek faks iş akışının veya TIFF arşivinin tipik olarak beklediği biçim olan CCITT Group 4 TIFF üretmez. Bunun nedeni somuttur, gözden kaçma değildir: PDFlibPas'ın CCITT birimi şu anda G4 stream'lerini decode eder ama G4 encoder içermez. Encoder olmayınca sıkıştırılmış monokrom run'ları yazacak yer yoktur; dolayısıyla monokrom yol sıkıştırılmamış 1-bit DIB'de biter

Pratikte bu yine de faydalıdır. 1-bit BMP doğru piksel biçimidir; dither edilmiştir ve hazırdır. Çoğu faks, arşiv veya OCR hattı bunu memnuniyetle içeri alır ya da tek bir aşağı akış adımıyla kendisi G4'e çevirir. Ama gereksiniminiz gerçekten kütüphaneden doğrudan Group 4 TIFF ise, henüz bu o değildir ve kendi sıkıştırma aşamanızı planlamalısınız. Özelliğin ne yaptığını bilmek kadar, nerede bittiğini bilmek de değerlidir

Her iki yöntem de bilinçli olarak küçüktür ve bu sayfadan alınması gereken tasarım dersi budur: renderer üstüne oturan convenience API, rasterizer'a girmeden ve diğer tüm çağıranları kararsızlaştırmadan gerçek yetenek ekleyebilir; monokrom çıktı ve bölge kırpma gibi. Alttaki rasterization için rendering motorları arasında seçim yapmanız gerektiğinde, Delphi'de çok motorlu PDF rendering genel bakışı ödünleşimleri derinlemesine ele alır. Tam rendering yüzeyi ve API'nin geri kalanı için PDFlibPas Delphi PDF Library ürün sayfası bütün resmi sunar