Taranmış bir arşiv, tek bir PDF içinde birkaç gigabayta ulaşabilir. Böyle bir dosyayı açan bir görüntüleyici genellikle tek bir sayfayı, belki içindekiler tablosunu, belki de kullanıcının bir yer iminden atladığı bir sayfayı göstermek ister. İki sayfa görüntülemek için tüm dosyayı belleğe okumak her eksende savurgandır: adres alanını tüketir, kullanıcıyı uzun bir başlangıç okumasının arkasında bekletir ve 32 bit bir Delphi işleminde tek bir sayfa görünmeden önce baştan başarısız olabilir. PDFium bu düşünülerek inşa edilmiştir. Bir belgeyi, ihtiyaç duyduğu belirli bayt aralıklarını, ihtiyaç duyduğu anda isteyen bir geri çağrı (callback) aracılığıyla yükleyebilir ve hiçbir zaman dosyanın tamamını bir seferde talep etmez. Bir sınır baştan belirtilmeli: bu akıtma kanalı dosyayı 32 bit bir uzunlukla tanımlar, dolayısıyla 4 GiB'ye kadar tek bir dosyaya hizmet eder ki bu, pratikte neredeyse her taranmış arşivi kapsar. Bu çizginin ötesindeki bir dosya bu makalenin alanı değildir; tarama sırasında ciltlere bölünmek ya da bunun yerine doğrudan erişim (direct-access) stratejisiyle açılmak ister ve tavanı dürüstçe zorunlu kılan koruma da aşağıda kendine ait bir bölüm alır
Bileşen o yolu bir akış bağdaştırıcısı aracılığıyla sunar. Ona herhangi bir TStream verirsiniz ve PDFium bloklarını o akıştan istek üzerine çeker. Dosya diskte, bir veritabanı blob alanında ya da başka herhangi bir TStream türevinin arkasında durabilir ve bunların hiçbiri baştan belleğe kopyalanmaz
PDFium baytları nasıl ister
PDFium'un C API'si, bir belgeyi FPDF_FILEACCESS yapısıyla tanımlanan, çağıran tarafından sağlanan bir nesneden yükler. Yapının burada önemli olan üç parçası vardır: bir uzunluk alanı, bir okuma geri çağrısı ve opak bir kullanıcı parametresi. Onu tüketen giriş noktası FPDF_LoadCustomDocument'tır. PDFium o yapıyı tuttuktan sonra artbilgiyi (trailer) ayrıştırır, çapraz başvuru tablosunu bulur ve o andan itibaren yalnızca belirli bir işlemin gerektirdiğini okur. Belgeyi açmak dosyanın kuyruğuna ve bir avuç katalog nesnesine dokunur. 400. sayfayı işlemek (render) o sayfanın içerik akışlarını ve kaynaklarını okur, başka hiçbir şeyi değil
Bu, tamponlanmış bir yükleme ile akıtmalı bir yükleme arasındaki farktır. Tamponlanmış bir yükleme, PDFium sıfırıncı baytı görmeden önce dosyayı baştan sona okur. Akıtmalı bir yükleme ilişkiyi tersine çevirir: PDFium okumaları yönlendirir ve hiç dokunulmayan baytlar hiç okunmaz. Bir seferde bir sayfa görüntülenen çok gigabaytlık bir dosya için bu, kullanılamaz bir yükleme ile anlık bir yükleme arasındaki uçurumdur
Akış bağdaştırıcısı
Bir Delphi TStream'i FPDF_FILEACCESS'e köprüleyen bağdaştırıcı TPdfStreamAdapter'dır. Kurucusu (constructor) akışı ve bir sahiplik bayrağını alır, akış uzunluğunu bir kez yakalar, FPDF_FILEACCESS kaydını doldurur ve okuma geri çağrısını bağlar. PDFium daha sonra bir ofset ve boyutla geri çağrı yaptığında, bağdaştırıcı akışı o ofsete konumlandırır ve tam olarak o aralığı PDFium'un sağladığı tampona kopyalar
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
Sahiplik bayrağı akışı kimin serbest bırakacağına karar verir. False geçin ve çağıran taraf akışı elinde tutar ve onu belgenin tüm ömrü boyunca canlı tutmalıdır. True geçin ve bağdaştırıcı devralır, belge kapandığında akışı serbest bırakır. Her iki durumda da akış, PDFium'un gerçekleştireceği her okumadan daha uzun yaşamalıdır, çünkü PDFium FPDF_FILEACCESS işaretçisini tutar ve belge açıkken yalnızca ilk yükleme sırasında değil, herhangi bir noktada geri çağrı yapabilir
Geri çağrının neden statik bir fonksiyon olduğu
PDFium'un m_GetBlock'ta sakladığı okuma geri çağrısı, cdecl çağrı kuralına sahip düz bir C fonksiyon işaretçisidir. Bir Delphi metodu doğrudan kullanılamaz, çünkü bir metot, bir C çağırıcısının hiçbir şey bilmediği ve asla sağlamayacağı gizli bir Self argümanı taşır. Bu yüzden bağdaştırıcı, geri çağrıyı cdecl; static olarak işaretlenmiş bir class function olarak bildirir; bu, PDFium'un beklediği C çerçeve düzeni ile serbest duran bir fonksiyona ve örtük bir Self olmadan derlenir
Bu, çağrı kuralını çözer ancak ikinci bir soru doğurur: Self olmadan, geri çağrı okuması gereken belirli akışa nasıl ulaşır? Cevap opak kullanıcı parametresidir. Bağdaştırıcı kaydı oluşturduğunda kendi örnek işaretçisini m_Param'da saklar. PDFium her geri çağrının ilk argümanı olarak aynı işaretçiyi geri verir. Statik fonksiyon onu tekrar bir TPdfStreamAdapter'a dönüştürür ve okumayı o örneğin akışına karşı yönlendirir. Bu, nesne kavramı olmayan bir C sınırı boyunca nesne bağlamını taşımak için kullanılan standart trambolindir
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
4 GiB tavanı ve neden bir korumaya ihtiyaç duyduğu
İşte girişte belirtilen sınırın geldiği yer burasıdır. FPDF_FILEACCESS'teki m_FileLen uzunluk alanı 32 bit işaretsiz bir değerdir. Temsil edebileceği en büyük uzunluk 4 GiB'den bir bayt eksiktir. Bir TStream, boyutunu bir Int64 olarak bildirir, dolayısıyla bir akış, alanın tutabileceğinden çok daha fazla baytı tanımlayabilir. Bir akışın boyutu o tavanı aştığı an, PDFium'a dosyanın ne kadar uzun olduğunu söylemenin dürüst bir yolu yoktur
Yanlış tepki, boyutu atamak ve onun sarmalanmasına (wrap) izin vermektir. 5 GiB'lik bir uzunluğu 32 bit bir alana kırpmak küçük, makul görünen bir sayı üretir ve PDFium ardından dosyayı kabaca bir gigabaytta bittiğine inanarak ayrıştırır. Artbilgi ve çapraz başvuru tablosu dosyanın gerçek sonunda, kırpılmış uzunluğun çok ötesinde bulunur, dolayısıyla ayrıştırma gerçek nedenle hiçbir ilgisi olmayan bir şekilde başarısız olur. İki katman yukarıda bir tamsayının sarmalandığına dair hiçbir ipucu olmadan, kusursuzca geçerli bir dosyada bir çapraz başvuru hatasını ayıklamış olursunuz
Bağdaştırıcı bunun yerine girdiyi reddeder. Kurucu, akış boyutunu High(FPDF_DWORD)'a karşı karşılaştırır ve akış tanımlanamayacak kadar büyük olduğu an EPdfError fırlatır. Açık, anlık bir hata gerçek sorunu inşa noktasında adlandırır. Sessiz bir kırpma ise onu çok daha sonra kovalayacağınız yanıltıcı bir belirtinin arkasına saklar. 4 GiB sınırı bu yükleme yolunun gerçek bir kısıtıdır ve dürüst olan şey, onu tesadüfen derlenen bir aritmetikle örtbas etmek yerine yüksek sesle yüzeye çıkarmaktır. Bir arşiv gerçekten çizgiyi aştığında, en tepede vaat edilen çözümler bu API'nin dışında yer alır: taramayı her biri tavanın altında kalan cilt başına dosyalara bölün ya da belgeyi diskte bırakın ve onu FPDF_FILEACCESS yerine 64 bit ofsetler üzerine inşa edilmiş bir doğrudan erişim tasarımı aracılığıyla sunun
Başarısızlıklar sınırı geçmemeli
Bir okuma başarısız olabilir. Akış, zaman aşımına uğrayan ağ destekli bir nesne, altınızdan kapatılan bir blob tanıtıcısı ya da belge açıldıktan sonra kırpılan bir dosya olabilir. PDFium'un okuma geri çağrısı için sözleşmesi bir dönüş değeridir: başarı için sıfırdan farklı, başarısızlık için sıfır. Bu bir C çerçevesidir ve bir Pascal istisnasını yakalayacak veya yayacak hiçbir mekanizması yoktur
Bu yüzden trambolin, arama ve okumayı, istisnayı yutan ve sıfır döndüren bir try/except içine sarar. Bir Delphi istisnasının geri çağrının dışına yayılmasına izin verilseydi, hiçbir zaman Pascal istisna mekanizması tarafından geri sarılmak üzere inşa edilmemiş PDFium'un cdecl yığın çerçeveleri arasından geriye doğru sarılırdı. Sonuç en iyi ihtimalle tanımsız davranış, en kötü ihtimalle kullanılabilir bir yığın olmadan PDF ayrıştırıcısının derinliklerinde sert bir çökmedir. Sıfır döndürmek başarısızlığı sözleşmenin içinde tutar. PDFium başarısız bir blok okuması görür, işlemi temiz bir şekilde iptal eder ve FPDF_LoadCustomDocument, belgenin yüklenemediğini bildirir; bileşen bunu ait olduğu Pascal tarafında bir EPdfError olarak yüzeye çıkarır
Bir belgeyi bu şekilde açma
Akıtma yolunu yöneten bileşen metodu, bir TMemoryStream geçmenin asla yanlışlıkla tamponlanmış yola düşmemesi için başka bir LoadDocument aşırı yüklemesi (overload) yerine ayrı bir metot olarak bildirilen LoadCustomDocument'tır. Bağdaştırıcıyı oluşturur, FPDF_LoadCustomDocument'ı çağırır ve bağdaştırıcıyı yüklenen belgenin ömrü boyunca canlı tutar
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
Aynı çağrı bir TMemoryStream, bir veritabanı veri kümesinden gelen bir blob akışı ya da özel bir TStream türevi için de çalışır. İstek üzerine yükleme, dosya büyük olduğunda ve yalnızca bir kısmı okunacağında hakkını verir: bir arşiv görüntüleyicisi, birkaç sayfa örnekleyen bir küçük resim üreteci, bir seferde bir sayfa çeken bir arama dizini. Dosya küçük olduğunda ya da zaten hepsini okuyacaksanız, tamponlanmış bir yükleme daha basittir ve akıtma mekanizması size bir şey kazandırmaz. Belirleyici etken, gerçekte dokunacağınız baytların dosyanın içerdiği baytlara oranıdır
Sayfalar istek üzerine akmaya başladıktan sonra, bir sonraki endişe kullanıcı yakınlaştırıp kaydırırken işlenmiş sayfaları duyarlı tutmaktır ki bu, işleme önbelleği ve yakınlaştırma performansı üzerine notumuzda ele alınmaktadır. Akıtılan belge, bir görüntüleyicinin göstermesi gereken ancak kullanıcının dışa aktarmasına veya değiştirmesine izin vermemesi gereken bir belge olduğunda, güvenli PDF önizleme adım adım kılavuzundaki teknikler bu yükleme yoluyla doğal olarak eşleşir. Her ikisi de burada açıklanan akıtmalı yükleme üzerine kuruludur ve bu, bu blogun başka yerlerinde ele alınan işleme, metin çıkarma ve açıklama API'lerinin yanında Delphi ve C++Builder için PDFium Component'in bir parçası olarak gelir