PDFium Component отваря PDF, който още се тегли, чрез TPdfProgressiveDocument — подклас на TPdf, който опакова availability API-я на PDFium FPDFAvail_*. BeginProgressiveLoad стартира сесията, CheckDocumentAvailability докладва кои байт диапазони PDFium още иска, OpenProgressiveDocument отваря файла, щом има достатъчно байтове, а CancelProgressiveLoad изоставя прекъснато теглене, без да пропуска нативни handle-и. Трудното не е щастливият път. Viewer на несигурна връзка ще види потребители, които затварят таба на 25 процента, размислят и отварят същата връзка пак, а всяка от тези прекъснати сесии носи нативен availability handle, два C callback записа, stream адаптер и куп in-flight range заявки, които трябва да се освободят в точно правилния ред
Как TPdfProgressiveDocument зарежда PDF, който още се тегли?
TPdfProgressiveDocument държи PDFium availability provider жив, докато random-access stream се пълни, и пита този provider преди всяка стъпка на парсване дали търсените байтове са налични. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) взима опорния stream плюс логическия размер на отдалечения файл, закача callback IsDataAvail и callback AddSegment в два записа и вика FPDFAvail_Create. Когато PDFium пита дали даден диапазон е наличен, компонентът отговаря да, ако диапазонът лежи вътре в contiguous префикса, описан от AvailableByteCount, или вътре в диапазон, вече завършен през scheduler-а RangeRequests, а event-ът OnDataAvailable може да отмени присъдата за sparse хранилища. Всяко извикване на CheckDocumentAvailability връща една от трите стойности TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) и подава обратно диапазоните, които PDFium е поискал, като сортиран, слепен масив TPdfDownloadRanges, вече на опашка в scheduler-а с приоритет rrpImmediate
// FetchRange е вашият транспорт (HTTP Range GET, socket, blob reader):
// записва Size байта на Offset в Store и връща колко са пристигнали
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;
// Намеците вече са на опашка; първо запишете байтовете, после завършете
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;
Две подробности в този цикъл са товароносещи. Таванът на рундовете има значение, защото мъртва връзка кара CheckDocumentAvailability да иска вечно същите диапазони, а неограничен цикъл превръща мрежов провал в закачен UI. Редът има значение, защото scheduler-ът сериализира собствения си статус с critical section, но не прави нищо за TStream.Position на опорното хранилище: транспортна нишка трябва да запише байтовете от отговора в stream-а, преди да извика CompleteRequest, защото от момента, в който завършването е публикувано, PDFium може да прочете този диапазон, а конкурентните писачи се нуждаят от positioned I/O или собствена ключалка
Защо AvailableByteCount отказва да върви назад?
AvailableByteCount само расте, а setter-ът вдига EPdfError с „Available byte count cannot move backwards“, когато се опитате да го смалите. Щом callback-ът IsDataAvail е казал на PDFium, че диапазон съществува, parser-ът може вече да е чел и кеширал обекти от него, така че оттеглянето на тези байтове след това би направило отговорите за наличност непоследователни спрямо онова, което PDFium вече е консумирал. Същият setter отхвърля стойности, по-големи от LogicalFileSize, и вдига „No progressive load is active“ извън сесия — затова байтовете, които вече държите, преди зареждането да е тръгнало, принадлежат в аргумента AInitialAvailableByteCount на BeginProgressiveLoad, а не в присвояване на свойство, направено твърде рано. Ако хранилището за теглене се пълни безредно, изобщо не се опитвайте да го изразите през префикса: завършвайте диапазоните през scheduler-а или отговаряйте през OnDataAvailable
Кога частично теглен PDF действително може да се отвори?
Само linearized PDF (Annex F на ISO 32000-1, подредбата „Fast Web View“) се отваря, преди целият файл да е пристигнал; не-linearized PDF все още иска всеки байт. OpenProgressiveDocument проверява свойството Linearization (plnUnknown, plnNotLinearized, plnLinearized) и насочва съответно: linearized файл се отваря чрез FPDFAvail_GetDocument щом секцията на първа страница и hint таблиците са налични, докато не-linearized файл се отваря чрез FPDF_LoadCustomDocument върху същия запис за достъп до файла и се приема за четим само като цяло. Насочването съществува по конкретна причина. Извикване на FPDFAvail_GetDocument върху не-linearized файл може да върне non-null handle с брой страници нула — документ, който изглежда отворен и е празен. В собствения тест suite на компонента 51-страничен linearized fixture стига pdaAvailable и се отваря с пълното си page дърво, докато sparse хранилището за теглене все още не покрива файла
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 вече е активната страница
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage взима номер на страница 1-based и налага реда, който PDFium очаква: преди първата проверка на страница изпълнява CheckFormAvailability, който опакова FPDFAvail_IsFormAvail, и чак след това вика FPDFAvail_IsPageAvail. Резултат pfaNotPresent е нормалният отговор за документ без AcroForm и не блокира нищо. Щом страницата е готова, LoadAvailablePage я прави активна, така че viewer може да рендира страница 1 на linearized брошура, докато останалите страници са още в път; FirstAvailablePageNumber казва коя страница linearization речникът определя за първа, вече конвертирана от zero-based индекса на PDFium
Какво освобождава CancelProgressiveLoad и в какъв ред?
CancelProgressiveLoad разглобява сесия в четири стъпки, които не могат да се пренаредят: отмяна на range scheduler-а, затваряне на документа, унищожаване на availability handle-а с FPDFAvail_Destroy, после освобождаване на callback записите и stream адаптера. Отмяната на scheduler-а първо вдига generation броеча му, изхвърля всяка чакаща и in-flight заявка и палит OnCancelRequest за всяка in-flight, така че транспортно завършване, кацнало по-късно, носи старото поколение и CompleteRequest връща False, без да пипа нищо. Документът трябва да се затвори, преди availability handle-ът и адаптерът да си отидат, защото PDFium може да вика обратно към file-access provider-а, докато затваря документ, а ако адаптерът вече го няма, този callback чете освободена памет
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Scheduler-ът живее колкото FPdf, така че закачете го веднъж
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // вашият код: затворете този socket или запрос
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Методът е idempotent и е единственият cleanup път за три ситуации: BeginProgressiveLoad, провалил се по средата на конструкцията, изрична отмяна от потребителя и destructor-ът. BeginProgressiveLoad също го вика, преди да започне, така че рестартиране на същия обект върху нов URL е безопасно без изрична отмяна. Едно решение за собствеността е ваше да го вземете правилно: ако worker нишка пише в опорния stream, подавайте AOwnsStream = False и освобождавайте stream-а сами, след като worker-ът е спрял, защото при предадена собственост отмяната освобождава stream-а, докато късно писане може все още да е по пътя. Exception-ите, вдигнати в OnCancelRequest, се поглъщат на заявка, така че един провалящ се транспорт не може да блокира останалите отмяни
Как lifecycle suite-ът доказва, че cancel пътят не пропуска?
Lifecycle stress suite-ът на PDFium Component подлага на изследване прекъснато теглене в мрежов стил на всеки смесен цикъл. Всеки цикъл стартира progressive load, чието хранилище държи само четвъртина от байтовете на fixture-а, изисква pdaNotAvailable с непразен списък намеци, вика CancelProgressiveLoad и assert-ва, че обектът докладва нито ProgressiveLoading, нито Active; после пуска същия streaming път до край с пълна наличност, OpenProgressiveDocument, рендериране и затваряне. Смесеният прогон по подразбиране покрива 100 измерени цикъла с 600 отваряния, 2300 рендерирания и 100 progressive отмяни, а семплираната частна памет е пораснала с 8.21 MiB срещу бюджет 32 MiB. Suite-ът брои progressive отмяните отделно от отмяните на render callback, защото прекъснато теглене и render цикъл, спрял рано, са различни събития с различни критерии за приемане
Къде progressive пътят спира да помага
Няколко граници си заслужава да знаете, преди да построите viewer върху това. Функциите, които имат нужда от оригиналните байтове на файла, отказват непълен progressive източник, вместо да гадаят: ReadXmpPacket се проваля изрично, а signature валидацията докладва Indeterminate, докато целият файл е наличен. Тестът за наличност по подразбиране приема contiguous префикс, така че транспорт, теглещ диапазони безредно, трябва да ги завършва през RangeRequests или да отговаря през OnDataAvailable, иначе PDFium ще продължи да иска байтове, които вече държите. Не-linearized файл не печели нищо по време до първа страница, така че ако бързият първи paint има значение, linearize-те файла от страна на сървъра. А CancelProgressiveLoad не затваря socket-ите ви сам; OnCancelRequest е куката, където това става
За обикновенния път със stream адаптер, който зарежда пълен локален файл при поискване, вижте streaming на големи PDF при поискване с PDFium; за отваряне на PDF, седящ в по-голям буфер, вижте byte range зареждане за embedded PDF-и. Отмяната на бавно рендериране на вече заредена страница е отделен механизъм, разгледан в отменяемо progressive рендериране на страници. TPdfProgressiveDocument и неговият range scheduler пътуват с PDFium Component за Delphi и C++Builder