Rahat bir okuma yakınlaştırmasında oluşturulan tek bir A4 sayfası, birkaç megabaytlık 32 bit bitmap boyutundadır. Bunu 400 sayfalık bir sözleşmeyle çarptığınızda aritmetik soyut olmaktan çıkar: Her sayfayı önceden oluşturursanız, Windows'tan kullanıcının her seferinde tek bir ekran dolusuna bakacağı bir gigabaytın üzerinde bitmap istiyorsunuz demektir. Uygulama ya 32 bitlik bir yapıda adres alanı yetersizliğinden çöker ya da GPU ve sayfa ayrıştırıcı henüz kimsenin kaydırmadığı sayfaları işlerken ilk birkaç saniyesini donmuş olarak geçirir. Sürekli kaydırmalı (continuous-scroll) bir okuyucu, uzun tek bir sayfa şeridi gibi hissettirmelidir, ancak aslında hepsini aynı anda bellekte tutamaz
Bu gerilim buradaki asıl sorundur. PDFium Bileşeni bunu TPdfView içinde çözer, bu nedenle işin çoğu doğru ekran modunu seçmek ve bileşenin sizin adınıza ne yaptığını anlamaktır. Okuma akışı için sayfaları boyutlandırmak ve hızlı kaydırmayı duyarlı tutmak gibi sizin yerinize yapmadığı kısımlar, küçük bir kodun işe yaradığı yerlerdir. Çevreleyen öğeleri (araç çubuğu, küçük resimler, arama kutusu) hala bir araya getiriyorsanız, zengin özellikli görüntüleyici kılavuzu bu konuyu kapsar; burada konu kaydırmanın kendisidir
Düzen bir ekran modudur (display mode), bitmap paneli değil
VCL form çalışmalarındaki içgüdü, bir kaydırma kutusuna (scroll box) ulaşmak ve her sayfa için bir tane olacak şekilde resim kontrollerini içine yığmaktır. Buna direnin. Bu tasarım sizi sayfa konumlandırmayı, kaydırma matematiğini ve bellek sorununu aynı anda üstlenmeye zorlar ve her birini kötü bir şekilde yeniden icat edersiniz. TPdfView zaten belgeyi sürekli bir sayfalar dizisi olarak modeller ve düzeni DisplayMode özelliği aracılığıyla sunar
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // tek sayfa genişliğinde, dikey olarak kayar
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Dosya açılamadı');
Bu, sürekli kaydırma kurulumunun tamamıdır. dmSingleContinuous sayfaları, aralarındaki boşluklar dahili olarak işlenerek tek bir dikey sütun halinde düzenler ve görünüm bu sütun boyunca tek bir yüzey olarak kayar. Bağlanacak sayfa başına kontrol ve sıradan gezinme için yazılacak kaydırma işleyicisi (scroll handler) yoktur. Atamadan sonra Pdf.Active kontrolüne dikkat edin: Bir belgeyi açmak asla hata fırlatmaz, bu nedenle hasarlı veya şifre korumalı bir dosya Active değerini yakalanacak bir istisna olmaksızın False olarak bırakır ve bu kontrolü atlayan bir görüntüleyici boş bir panel işler ve hatayı kendinde arar
Aynı özellik yayma modlarını (spread modes) da taşır. dmTwoPageContinuous sayfaları, bazı belgelerin istediği kitap tarzı okuma için yan yana, her satıra iki tane olacak şekilde yerleştirir; dmTwoPageContinuousWithCover aynısını yapar ancak kalan yayılmaların doğal çift-tek sınırına düşmesi için birinci sayfanın kapak olarak tek başına durmasına izin verir. Her üçü de sürekli kayar. Aralarında geçiş yapmak tek bir atamadır, bu da daha sonra bir ekran modu kombo kutusu eklemeyi önemsiz kılar
Yalnızca görünür sayfalar rasterleştirilir
Bunun 400 sayfalık bir dosyaya ölçeklenmesinin nedeni, sütunun sanal olmasıdır. TPdfView belgenin sayfa ağacından her sayfanın yüksekliğini bilir, böylece hiçbir şeyi rasterleştirmeden toplam kaydırma kapsamını ve her sayfanın konumunu hesaplayabilir. Bir sayfanın içerik akışını piksellere dönüştüren pahalı adım olan rasterleştirme, yalnızca o anda görünüm penceresiyle (viewport) kesişen sayfalar ve ayrıca sayfa görünür hale gelene kadar hazır olması için küçük bir kenar boşluğu için gerçekleşir. Aşağı kaydırdıkça, görünüm penceresine giren sayfalar oluşturulur ve oradan çıkan sayfaların bitmap'leri serbest bırakılır. Bellek, belge uzunluğuna değil, ekrana sığan miktarla orantılı kalır
Bunu içselleştirmeye değer çünkü maliyet hakkında nasıl akıl yürüttüğünüzü değiştirir. 400 sayfalık bir belgeyi açmak ucuzdur: İçeriği değil, yapıyı ayrıştırır. Masraf sayfa başınadır ve tembelce (lazy), bir sayfa yakınına kaydırıldığı anda ödenir. Açılışta anında tepki veren ve kaydırırken pürüzsüz hissettiren bir görüntüleyici genel olarak daha az iş yapmıyor, işi kullanıcının gerçek okuma yoluna yayıyor ve geride kalanları atıyor demektir. Pratik sonuç, neredeyse hiçbir zaman kullanıcının önündeki sayfaları zorla oluşturmak istememenizdir. Bırakın neyin görünür olacağına görünüm karar versin
Sayfaları genişliğe sığdırın, ardından yakınlaştırmayı kendi haline bırakın
Bir okuma sütunu, mutlak bir yakınlaştırmaya sabitlenmiş sayfaları değil, panel genişliğine göre boyutlandırılmış sayfaları ister. FitMode bunu yapar ve pencere yeniden boyutlandırıldıkça yapmaya devam eder
PdfView.FitMode := pfmFitWidth; // her sayfa sütun genişliğini doldurur; yükseklik onu takip eder
pfmFitWidth ile bileşen, görünüm her yeniden boyutlandırıldığında yakınlaştırmayı yeniden hesaplar, böylece sütun her zaman mevcut genişliği doldurur ve sayfa yükseklikleri, dolayısıyla kaydırma kapsamı bunu takip eder. İnsanları yakalayan bir tuzak vardır: Doğrudan Zoom atamak FitMode değerini tekrar pfmNone durumuna sıfırlar. Bu kasıtlıdır, çünkü manuel yakınlaştırma ile otomatik sığdırma çelişkili niyetlerdir, ancak bu durum kodunuzda bir yerdeki başıboş bir PdfView.Zoom := 1.0 satırının genişliğe sığdırmayı sessizce kapattığı ve bir sonraki yeniden boyutlandırmanın akışı durdurduğu anlamına gelir. Hem yakınlaştırma kontrolü hem de sığdırma düğmesi sunuyorsanız, bunları bir mod anahtarı olarak ele alın: Birini ayarlamak diğerini temizler ve hangisinin kazanacağına siz karar verirsiniz
Doğal olarak okunan mutlak yakınlaştırma kontrolleri için görünüm, sığdırma yakınlaştırmalarını uygulayabileceğiniz veya görüntüleyebileceğiniz değerler olarak sunar: PageWidthZoom[PageNumber] o sayfayı genişliğe sığdıracak yakınlaştırmayı döndürür ve eşleşen PageZoom tüm sayfayı sığdırır. Bunları okumak, yatay veya aşırı büyük sayfalarda yanlış giden sihirli yüzdeleri sabit kodlamadan (hard-coding) bir "Genişliğe Sığdır" / "Sayfaya Sığdır" menüsünü nasıl dolduracağınızdır
Aşamalı oluşturma (progressive rendering) ile hızlı kaydırmayı duyarlı tutun
Varsayılan oluşturma (render) yolu, geri dönmeden önce bir sayfayı tamamlanana kadar çizer. Tek bir sayfa için bu sorun değildir. Yoğun bir belgede hızlıca kaydırma yaparken ise öyle değildir: Yanından geçip giden her sayfa tam bir rasterleştirmeyi başlatır ve kullanıcı sayfaların oluşturulabileceğinden daha hızlı kaydırıyorsa, bu oluşturmalar birikir ve panel kekeler; çünkü tamamlandığında zaten ekran dışında kalmış sayfalar için çalışma yapılıyordu. Düzeltme, oluşturmayı iptal edilebilir kılmak ve kullanıcının devam ettiği anda onu terk etmektir
RenderPageProgressive parçalar halinde oluşturur ve her parça sınırında bir iptal belirtecini (cancellation token) kontrol eder; böylece az önce kayıp giden bir sayfanın devam eden oluşturulması, sonuna kadar çalıştırılmak yerine iptal edilebilir
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Oluşturulmakta olan şeyi iptal et; eski belirteç artık sinyallendi.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap tamamlandı, boya
prsCancelled: Exit; // yerine yenisi geldi, bu sonucu at
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
Önemli olan şekil, geri dönüş değeridir. prsDone bitmap'in tamamen boyandığı ve ekrana aktarılmaya (blit) değer olduğu anlamına gelir; prsCancelled daha yeni bir kaydırma konumunun bu sayfanın yerine geçtiği anlamına gelir, bu nedenle kısmi sonucu göstermek yerine çöpe atarsınız; prsFailed o sayfadaki gerçek bir hatadır. İptal, önleyici olarak değil, parça sınırlarında sorgulanır; bu nedenle Cancel'ı çağırmak ile oluşturmanın fiilen durması arasında onlarca milisaniye gecikme bekleyin. Bu, eski bir tam sayfa oluşturmanın kuyruğu engellemesine izin vermekten hala çok daha ucuzdur. Belirteç olarak nil geçmek, iptal edilecek bir şeyin olmadığı baskı önizleme gibi tek seferlik bir oluşturma için doğru seçim olan tamamlanmaya kadar doğrudan oluşturur
Bunun yerine taze bir TBitmap döndüren RenderPage işlev formunu çağırdığınızda, çağıranın buna sahip olduğunu ve Free etmesi gerektiğini unutmayın. Sayfa başına bir bitmap ayıran bir kaydırma döngüsünde bunu unutmak, kullanıcının geçtiği her sayfayla büyüyen bir sızıntıdır; bu da sürekli tasarımın kaçınması gereken sınırsız bellek hatasının ta kendisidir. Mümkün olduğunda yeniden kullanılan bir bitmap içine oluşturun
Elde kalanlar
Sürekli kaydırmalı okuyucu, çoğunlukla bileşenin sunacağı bir şeydir. Düzen için dmSingleContinuous'u seçersiniz, sütunun pencereyle birlikte yeniden akması için pfmFitWidth'i ayarlarsınız ve bozuk bir dosyanın yüksek sesle başarısız olması için Pdf.Active'i kontrol edersiniz. Kendiniz yazmaya değer tek parça iptal edilebilir oluşturmadır (cancellable rendering), çünkü bir okuyucu, birisi kaydırma çubuğunu uzun bir belgenin altına sürüklediğinde ve panelin ayak uydurup uyduramadığında gösterdiği davranışa göre değerlendirilir. Bunun ötesindeki her şey; sayfalar arası metin seçimi, arama vurgulama, yer işareti ağacı, bu kaydırma yüzeyinin içinde değil, üzerinde yer alan arayüz çalışmalarıdır
Burada gösterilen TPdfView, DisplayMode ve RenderPageProgressive API'leri Delphi ve Lazarus için PDFium Bileşeni'nin bir parçasıdır