Taranmış bir arşiv, tek bir PDF'te 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 işaretinden atladığı bir sayfayı göstermek ister. İki sayfayı işlemek için tüm dosyayı belleğe okumak her eksende israftır: adres alanını yakar, kullanıcıyı uzun bir ilk okumanın arkasında bekletir ve 32 bitlik bir Delphi işleminde tek bir sayfa görünmeden önce tamamen başarısız olabilir. PDFium bu durum düşünülerek oluşturulmuştur. İhtiyaç duyduğu belirli bayt aralıklarını ihtiyaç duyduğu anda isteyen bir geri arama aracılığıyla bir belgeyi yükleyebilir ve asla tüm dosyayı bir kerede talep etmez
Bileşen bu yolu bir akış bağdaştırıcısı (stream adapter) aracılığıyla sunar. Ona herhangi bir TStream teslim edersiniz ve PDFium talep üzerine o akıştan blokları çeker. Dosya diskte, bir veritabanı blob alanında veya başka herhangi bir TStream alt sınıfının arkasında yer alabilir ve hiçbir şey önceden belleğe kopyalanmaz
PDFium baytları nasıl ister
PDFium'un C API'si, arayan tarafından sağlanan ve FPDF_FILEACCESS yapısı tarafından tanımlanan bir nesneden belge yükler. Yapının burada önemli olan üç kısmı vardır: uzunluk alanı, okuma geri araması ve opak bir kullanıcı parametresi. Onu tüketen giriş noktası FPDF_LoadCustomDocument'tır. PDFium bu yapıyı tuttuğunda, trailer'ı ayrıştırır, çapraz referans tablosunu konumlandırır ve o andan itibaren yalnızca belirli bir işlemin gerektirdiği şeyleri okur. Belgeyi açmak, dosyanın kuyruğuna ve bir avuç katalog nesnesine dokunur. 400. sayfayı işlemek, o sayfanın içerik akışlarını ve kaynaklarını okur, başka hiçbir şeyi okumaz
Arabelleğe alınmış (buffered) yükleme ile akışla (streaming) yükleme arasındaki fark budur. Arabelleğe alınmış bir yükleme, PDFium sıfırıncı baytı görmeden önce dosyayı uçtan uca okur. Akışla yükleme ise bu ilişkiyi tersine çevirir: PDFium okumaları yönlendirir ve hiç dokunulmayan baytlar asla okunmaz. Bir seferde bir sayfası görüntülenen çok gigabaytlık bir dosya için bu, kullanılamaz bir yükleme ile anında yükleme arasındaki farktır
Akış bağdaştırıcısı
Bir Delphi TStream nesnesini FPDF_FILEACCESS yapısına bağlayan bağdaştırıcı TPdfStreamAdapter'dır. Kurucusu akışı ve bir sahiplik bayrağını alır, akış uzunluğunu bir kez yakalar, FPDF_FILEACCESS kaydını doldurur ve okuma geri aramasını bağlar. PDFium daha sonra bir kayma (offset) ve boyutla geri çağırdığında, bağdaştırıcı akışı o kaymaya konumlandırır ve tam olarak o aralığı PDFium'un sağladığı arabelleğe 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 değerini geçerseniz, çağıran akışı saklar ve belgenin tüm ömrü boyunca onu canlı tutmalıdır. True değerini geçerseniz bağdaştırıcı devralır ve belge kapandığında akışı serbest bırakır. Her iki durumda da, akışın PDFium'un gerçekleştireceği her okumadan daha uzun yaşaması gerekir, çünkü PDFium FPDF_FILEACCESS işaretçisini tutar ve yalnızca ilk yükleme sırasında değil, belge açıkken herhangi bir noktada geri çağırır
Geri arama neden statik bir işlevdir
PDFium'un m_GetBlock içinde sakladığı okuma geri araması, cdecl çağırma kuralına sahip düz bir C işlev işaretçisidir. Bir Delphi yöntemi doğrudan kullanılamaz, çünkü bir yöntem, C çağıranının hakkında hiçbir şey bilmediği ve asla sağlamayacağı gizli bir Self bağımsız değişkeni taşır. Bağdaştırıcı bu nedenle geri aramayı cdecl; static olarak işaretlenmiş bir class function olarak bildirir; bu, PDFium'un beklediği C çerçeve düzenine sahip ve örtük Self içermeyen bağımsız bir işlev olarak derlenir
Bu, çağırma kuralını çözer ancak ikinci bir soruyu gündeme getirir: Self olmadan, geri arama 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 içinde saklar. PDFium, her geri aramanın ilk bağımsız değişkeni olarak aynı işaretçiyi geri verir. Statik işlev bunu bir TPdfStreamAdapter nesnesine geri dönüştürür ve okumayı o örneğin akışına karşı yürütür. Bu, nesne kavramı olmayan bir C sınırının ötesine nesne bağlamını aktarmak için 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
FPDF_FILEACCESS içindeki m_FileLen uzunluk alanı 32 bitlik işaretsiz bir değerdir. Temsil edilebilir en büyük uzunluğu, 4 GiB'tan bir bayt eksiktir. Bir TStream boyutunu bir Int64 olarak bildirir, bu nedenle bir akış alanın tutabileceğinden çok daha fazla baytı tanımlayabilir. Bir akışın boyutu bu tavanı aştığı an, PDFium'a dosyanın ne kadar uzun olduğunu söylemenin dürüst bir yolu yoktur
Yanlış yanıt, boyutu atamak ve sarmasına (wrap) izin vermektir. 5 GiB'lık bir uzunluğu 32 bitlik bir alana kırpmak küçük, makul görünen bir sayı üretir ve PDFium daha sonra dosyanın kabaca bir gigabaytta bittiğine inanarak dosyayı ayrıştırır. Trailer ve çapraz referans tablosu, kırpılmış uzunluğun çok ötesinde, dosyanın gerçek sonunda yaşar; bu nedenle ayrıştırma, gerçek neden ile hiçbir ilgisi olmayan bir şekilde başarısız olur. İki katman yukarıda bir tamsayının sardığına dair hiçbir ipucu olmadan, mükemmel derecede geçerli bir dosyada çapraz referans hatasını ayıklıyor olursunuz
Bağdaştırıcı bunun yerine girdiyi reddeder. Kurucu, akış boyutunu High(FPDF_DWORD) ile karşılaştırır ve akış tanımlanamayacak kadar büyük olduğu anda EPdfError oluşturur. Açık, acil bir hata, yapım noktasında gerçek sorunu adlandırır. Sessiz bir kırpma, bunu çok daha sonra kovalayacağınız yanıltıcı bir belirtinin arkasına gizler. 4 GiB sınırı bu yükleme yolunun gerçek bir kısıtlamasıdır ve dürüst olan şey, derlenen aritmetikle bunu örtbas etmek yerine yüksek sesle yüzeye çıkarmaktır
Başarısızlıklar sınırı geçmemelidir
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 tutamacı veya belge açıldıktan sonra kırpılan bir dosya olabilir. PDFium'un okuma geri araması için sözleşmesi bir dönüş değeridir: başarı için sıfır dışı, başarısızlık için sıfır. Bu bir C çerçevesidir ve bir Pascal istisnasını yakalamak veya yaymak için hiçbir mekanizmaya sahip değildir
Bu nedenle trambolin, aramayı ve okumayı istisnayı yutan ve sıfır döndüren bir try/except içine sarar. Bir Delphi istisnasının geri aramadan dışarı yayılmasına izin verilseydi, Pascal istisna mekanizması tarafından geri sarılmak üzere asla oluşturulmamış olan PDFium'un cdecl yığın çerçeveleri boyunca geri sarılırdı. Sonuç, en iyi ihtimalle tanımsız davranış, en kötü ihtimalle ise 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 sonlandırır ve FPDF_LoadCustomDocument belgenin yüklenemediğini bildirir; bu da bileşenin ait olduğu Pascal tarafında bir EPdfError olarak yüzeye çıkmasını sağlar
Bir belgeyi bu şekilde açma
Akış yolunu yönlendiren bileşen yöntemi LoadCustomDocument'tır; bir TMemoryStream geçilmesinin asla yanlışlıkla arabelleğe alınmış yola düşmemesi için başka bir LoadDocument aşırı yüklemesi yerine farklı bir yöntem olarak bildirilmiştir. Bağdaştırıcıyi 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 alınan bir blob akışı veya özel bir TStream alt sınıfı için çalışır. Talep üzerine yükleme, dosya büyük olduğunda ve yalnızca bir kısmı okunacağında değerini kanıtlar: bir arşiv görüntüleyici, birkaç sayfayı örnekleyen bir küçük resim oluşturucu, bir seferde bir sayfayı çeken bir arama dizini. Dosya küçük olduğunda veya zaten tamamını okuyacaksanız, arabelleğe alınmış bir yükleme daha basittir ve akış mekanizması size hiçbir şey kazandırmaz. Belirleyici faktör, gerçekte dokunacağınız baytların dosyanın içerdiği baytlara oranıdır
Sayfalar talep üzerine akışla aktarıldıktan sonra, bir sonraki endişe, kullanıcı yakınlaştırıp kaydırırken işlenen sayfaların duyarlı kalmasını sağlamaktır; bu, işleme önbelleğe alma ve yakınlaştırma performansı hakkındaki notumuzda ele alınmıştır. Akışla aktarı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 kılavuzundaki teknikler bu yükleme yoluyla doğal olarak eşleşir. Her ikisi de, bu blogun başka yerlerinde ele alınan işleme, metin çıkarma ve açıklama API'lerinin yanı sıra Delphi ve C++Builder için PDFium Bileşeni'nin bir parçası olarak sunulan burada açıklanan akışla yükleme üzerine kurulmuştur