Teknik Makale

Delphi'de PDFium Progressive Download ve İptal (FPDFAvail)

PDFium Component, hâlâ inmekte olan bir PDF'i TPdfProgressiveDocument üzerinden açar; bu, PDFium'un FPDFAvail_* kullanılabilirlik API'sini saran bir TPdf alt sınıfıdır. BeginProgressiveLoad oturumu başlatır, CheckDocumentAvailability PDFium'un hâlâ hangi bayt aralıklarına ihtiyaç duyduğunu bildirir, OpenProgressiveDocument yeterli bayt var olduğunda dosyayı açar ve CancelProgressiveLoad yarım kalmış bir indirmeyi yerli tanıtıcıları sızdırmadan bırakır. Zor olan mutlu yol değildir. Kararsız bir bağlantıdaki bir görüntüleyicide kullanıcılar yüzde 25'te sekmeyi kapatır, fikrini değiştirir ve aynı bağlantıyı yeniden açar; bu yarıda kesilmiş oturumların her birinin, tam olarak doğru sırada serbest bırakılması gereken bir yerli kullanılabilirlik tanıtıcısı, iki C callback kaydı, bir akış adaptörü ve havadaki bir aralık isteği kümesi vardır

TPdfProgressiveDocument hâlâ inmekte olan bir PDF'i nasıl yükler?

TPdfProgressiveDocument, rastgele erişimli bir akış doldurulurken bir PDFium kullanılabilirlik sağlayıcısını canlı tutar ve her ayrıştırma adımından önce o sağlayıcıya istediği baytların mevcut olup olmadığını sorar. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount), destek akışını ve uzak dosyanın mantıksal boyutunu alır, bir IsDataAvail callback'i ile bir AddSegment callback'ini iki kayda bağlar ve FPDFAvail_Create çağırır. PDFium bir aralığın mevcut olup olmadığını sorduğunda bileşen, aralık AvailableByteCount'un betimlediği bitişik önekte ya da RangeRequests zamanlayıcısı üzerinden hâlihazırda tamamlanmış bir aralıkta ise evet der ve OnDataAvailable olayı, dağınık depolar için kararı geçersiz kılabilir. CheckDocumentAvailability'e her çağrı üç TPdfDataAvailability değerinden birini döndürür (pdaAvailable, pdaNotAvailable, pdaError) ve PDFium'un istediği aralıkları sıralanmış, birleştirilmiş bir TPdfDownloadRanges dizisi olarak verir; bunlar zaten rrpImmediate önceliğiyle zamanlayıcıya kuyruğa alınmıştır

// FetchRange sizin taşımanızdır (HTTP Range GET, soket, blob okuyucu):
// Store'a Offset'e Size bayt yazar ve kaç tanesinin geldiğini döndürür
function FetchRange(Store: TStream; Offset, Size: UInt64): UInt64; forward;

procedure OpenWhileDownloading(Pdf: TPdfProgressiveDocument; Store: TStream;
  RemoteSize: UInt64);
const
  MaxRounds = 64;
var
  Hints: TPdfDownloadRanges;
  State: TPdfDataAvailability;
  Request: TPdfRangeRequest;
  Round: Integer;
begin
  Pdf.BeginProgressiveLoad(Store, RemoteSize, False);
  State := pdaNotAvailable;
  for Round := 1 to MaxRounds do
  begin
    State := Pdf.CheckDocumentAvailability(Hints);
    if State <> pdaNotAvailable then
      Break;
    // İpuçları hâlihazırda kuyruktadır; önce baytları yaz, sonra tamamla
    while Pdf.RangeRequests.TryDequeue(Request) do
      Pdf.RangeRequests.CompleteRequest(Request,
        FetchRange(Store, Request.Offset, Request.Size));
  end;
  if State <> pdaAvailable then
    raise EPdfError.Create('The document could not be discovered');
  Pdf.OpenProgressiveDocument;
end;

O döngüdeki iki ayrıntı yük taşır. Tur sınırı önemlidir, çünkü ölü bir bağlantı CheckDocumentAvailability'nin sonsuza dek aynı aralıkları istemesine yol açar ve sınırsız bir döngü bir ağ başarısızlığını takılı bir arayüze çevirir. Sıralama önemlidir, çünkü zamanlayıcı kendi durumunu bir critical section ile serileştirir ama destek depodaki TStream.Position için hiçbir şey yapmaz: bir taşıma thread'i, CompleteRequest çağırmadan önce yanıt baytlarını akışa yazmalıdır; çünkü bir tamamlanma yayımlandığı anda PDFium o aralığı okuyabilir ve eşzamanlı yazanların konumlu I/O'ya ya da kendi kilitlerine ihtiyacı vardır

PDFium Component'te TPdfProgressiveDocument kullanılabilirlik döngüsü: BeginProgressiveLoad, FPDFAvail sağlayıcısını kurar; CheckDocumentAvailability, rrpImmediate önceliğiyle kuyruğa alınmış sıralı birleşik indirme ipuçlarını verir; taşıma, CompleteRequest her aralığı PDFium'a yayımlamadan önce baytları depoya yazar; döngü, ölü bir bağlantı aynı aralıkları istemeyi sürdüreceği için 64 turda sınırlıdır
Baytları yazın, sonra isteği tamamlayın: bir tamamlanma yayımlandığı anda PDFium o aralığı okuyabilir ve akış konumunu sizin için koruyan hiçbir şey yoktur

AvailableByteCount geriye gitmeyi neden reddeder?

AvailableByteCount yalnızca büyür ve setter, küçültmeye çalıştığınızda "Available byte count cannot move backwards" iletisiyle EPdfError yükseltir. IsDataAvail callback'i bir kez PDFium'a bir aralığın var olduğunu söylediyse, ayrıştırıcı o nesneleri hâlihazırda okuyup önbelleğe almış olabilir; bu baytları sonra geri çekmek, kullanılabilirlik yanıtlarını PDFium'un hâlihazırda tükettiğiyle tutarsız kılar. Aynı setter, LogicalFileSize'dan büyük değerleri de reddeder ve oturum dışında "No progressive load is active" yükseltir; bu yüzden yük başlamadan hâlihazırda tuttuğunuz baytlar, çok erken yapılan bir özellik atamasına değil, BeginProgressiveLoad'in AInitialAvailableByteCount argümanına aittir. İndirme deponuz düzensiz doluyorsa bunu hiçbir zaman önek üzerinden ifade etmeye çalışmayın: aralıkları zamanlayıcı üzerinden tamamlayın ya da OnDataAvailable üzerinden yanıtlayın

Kısmen inmiş bir PDF gerçekte ne zaman açılabilir?

Yalnızca linearized bir PDF (ISO 32000-1 Annex F, "Fast Web View" yerleşimi), dosyanın tamamı varmadan açılır; linearize edilmemiş bir PDF hâlâ her bayta ihtiyaç duyar. OpenProgressiveDocument Linearization özelliğini denetler (plnUnknown, plnNotLinearized, plnLinearized) ve buna göre yönlendirir: linearized bir dosya, ilk sayfa bölümü ile ipucu tabloları mevcut olur olmaz FPDFAvail_GetDocument üzerinden açılır; linearize edilmemiş dosya ise aynı dosya erişim kaydı üzerinde FPDF_LoadCustomDocument ile açılır ve yalnızca bir bütün olarak okunabilir sayılır. Yönlendirmenin somut bir nedeni vardır. Linearize edilmemiş bir dosyada FPDFAvail_GetDocument çağırmak, sayfa sayısı sıfır olan null olmayan bir tanıtıcı döndürebilir; açık görünen ama boş bir belge. Bileşenin kendi test paketinde 51 sayfalık linearized bir test verisi, dağınık indirme deposu dosyayı hâlâ kapsamazken pdaAvailable'e ulaşır ve tam sayfa ağacıyla açılır

OpenProgressiveDocument, PDFium Component'te kısmi bir indirmeyi nasıl yönlendirir: linearized dosya, ilk sayfa bölümü ile ipucu tabloları gelince FPDFAvail_GetDocument üzerinden açılır; linearize edilmemiş dosya FPDF_LoadCustomDocument ile her baytı ister; LoadAvailablePage, sayfa denetiminden önce form kullanılabilirliğini FPDFAvail_IsFormAvail ile denetler ve null olmayan sıfır sayfalı tanıtıcı tuzağından kaçınır
Yalnızca linearized dosyalar erken başlangıç kazanır; gerisinde FPDFAvail_GetDocument, sıfır sayfalı açık görünen bir belge döndürebilir ve yönlendirmenin engellediği de tam olarak budur
function WaitForPage(Pdf: TPdfProgressiveDocument; Store: TStream;
  PageNumber: Integer): Boolean;
var
  Hints: TPdfDownloadRanges;
  Request: TPdfRangeRequest;
  Round: Integer;
begin
  Result := False;
  for Round := 1 to 64 do
    case Pdf.LoadAvailablePage(PageNumber, Hints) of
      pdaAvailable:
        Exit(True);   // PageNumber artık etkin sayfadır
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage 1 tabanlı bir sayfa numarası alır ve PDFium'un beklediği sırayı zorlar: ilk sayfa denetiminden önce FPDFAvail_IsFormAvail'i saran CheckFormAvailability'i koşar ve ancak ondan sonra FPDFAvail_IsPageAvail'i çağırır. pfaNotPresent sonucu, AcroForm'suz bir belge için normal yanıttır ve hiçbir şeyi engellemez. Sayfa hazır olduğunda LoadAvailablePage onu etkin sayfa yapar; böylece bir görüntüleyici, kalan sayfalar hâlâ yoldayken linearized bir broşürün 1. sayfasını çizebilir; FirstAvailablePageNumber, linearization sözlüğünün hangi sayfayı ilk olarak gösterdiğini, PDFium'un 0 tabanlı indeksinden çevrilmiş olarak söyler

CancelProgressiveLoad neyi, hangi sırayla serbest bırakır?

CancelProgressiveLoad bir oturumu yeniden sıralanamayan dört adımda söker: aralık zamanlayıcısını iptal et, belgeyi kapat, kullanılabilirlik tanıtıcısını FPDFAvail_Destroy ile yok et, sonra callback kayıtlarını at ve akış adaptörünü serbest bırak. Önce zamanlayıcıyı iptal etmek, kuşak sayacını yükseltir, bekleyen ve havadaki her isteği düşürür ve havadaki her biri için OnCancelRequest ateşler; böylece sonra düşen bir taşıma tamamlanması eski kuşağı taşır ve CompleteRequest, hiçbir şeye dokunmadan False döner. Belgenin, kullanılabilirlik tanıtıcısı ile adaptör gitmeden önce kapatılması gerekir; çünkü PDFium bir belgeyi kapatırken dosya erişim sağlayıcısına geri çağrı yapabilir ve adaptör çoktan gittiyse o callback serbest bırakılmış belleği okur

PDFium Component'te CancelProgressiveLoad sabit sökme sırası: önce aralık zamanlayıcısını iptal et ki geç tamamlanmalar yükselen kuşak sayacına çarpsın ve False dönsün; dosya erişim adaptörü kaybolmadan belgeyi kapat; kullanılabilirlik tanıtıcısını FPDFAvail_Destroy ile yok et; ancak ondan sonra callback kayıtlarını at ve akış adaptörünü serbest bırak
Tek bir idempotent yöntem, başarısız bir başlangıcı, kullanıcı iptalini ve yıkıcıyı aynı biçimde temizler; depoya yazan bir worker thread varken akış sahipliği sizde kalır
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Zamanlayıcı FPdf ile aynı ömürlüdür; bir kez bağlayın
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // sizin kodunuz: o soketi ya da isteği kapat
end;

procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
  FPdf.CancelProgressiveLoad;
  // ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;

Yöntem idempotenttir ve üç durum için tek temizleme yoludur: yapımın ortasında başarısız olan bir BeginProgressiveLoad, açık bir kullanıcı iptali ve yıkıcı. BeginProgressiveLoad başlamadan önce onu çağırır; böylece aynı nesneyi yeni bir URL'de yeniden başlatmak, açık bir iptal olmaksızın güvenlidir. Doğru yapmanız gereken bir sahiplik kararı vardır: bir worker thread destek akışına yazıyorsa AOwnsStream = False geçirin ve akışı, worker durduktan sonra kendiniz serbest bırakın; çünkü sahiplik devredilmişken iptal, geç bir yazı hâlâ yoldayken akışı serbest bırakabilir. OnCancelRequest içinde yükselen istisnalar istek başına yutulur; böylece başarısız olan bir taşıma kalan iptalleri engelleyemez

Yaşam döngüsü paketi iptal yolunun sızdırmadığını nasıl kanıtlar?

PDFium Component yaşam döngüsü stres paketi, her karma döngüde kesintiye uğramış ağ tarzı bir indirmeyi çalıştırır. Her döngü, deposu test verisi baytlarının yalnızca dörtte birini tutan bir progressive load başlatır, boş olmayan ipucu listesiyle pdaNotAvailable ister, CancelProgressiveLoad çağırır ve nesnenin ne ProgressiveLoading ne Active bildirdiğini assert eder; sonra aynı akış yolunu tam kullanılabilirlikle sonuna dek koşturur: OpenProgressiveDocument, bir render ve bir kapatma. Varsayılan karma koşu, 600 açma, 2300 render ve 100 progressive iptalle 100 ölçümlü döngü kapsar ve örneklenen özel bellek, 32 MiB bütçeye karşı 8.21 MiB büyüdü. Paket progressive iptalleri render-callback iptallerinden ayrı sayar; çünkü yarıda kesilen bir indirme ile erken duran bir render döngüsü, farklı kabul ölçütleri olan farklı olaylardır

Progressive yolun artık yardımcı olmadığı yerler

Bunun üstüne bir görüntüleyici kurmadan önce bilmeye değer birkaç sınır var. Orijinal dosya baytlarına ihtiyaç duyan özellikler, eksik bir progressive kaynağı tahmin etmek yerine reddeder: ReadXmpPacket açıkça başarısız olur ve imza doğrulaması, dosyanın tamamı gelene dek Indeterminate bildirir. Varsayılan kullanılabilirlik sınaması bitişik bir önek varsayar; dolayısıyla aralıkları düzensiz getiren bir taşımanın onları RangeRequests üzerinden tamamlaması ya da OnDataAvailable üzerinden yanıtlaması gerekir, yoksa PDFium hâlihazırda tuttuğunuz baytları istemeye devam eder. Linearize edilmemiş bir dosya, ilk sayfaya kadar geçen sürede hiçbir şey kazanmaz; hızlı ilk çizim önemliyse dosyayı sunucu tarafında linearize edin. Bir de CancelProgressiveLoad soketlerinizi kendisi kapatmaz; OnCancelRequest bunun olduğu kanca tutamağıdır

Tam bir yerel dosyayı talep üzerine yükleyen düz akış adaptörü yolu için PDFium ile büyük PDF'leri talep üzerine akıtma makalesine bakın; daha büyük bir tamponun içinde duran bir PDF'i açmak için gömülü PDF'ler için bayt aralığı yükleme makalesine bakın. Hâlihazırda yüklenmiş bir sayfanın yavaş render'ını iptal etmek ayrı bir mekanizmadır ve iptal edilebilir progressive sayfa render'ı makalesinde kapsanıyor. TPdfProgressiveDocument ile aralık zamanlayıcısı, Delphi ve C++Builder için PDFium Component ile birlikte gelir