PDFium Component відкриває PDF, який ще завантажується, через TPdfProgressiveDocument — підклас TPdf, що обгортає availability API PDFium FPDFAvail_*. BeginProgressiveLoad стартує сесію, CheckDocumentAvailability звітує, які діапазони байтів PDFium ще потребує, OpenProgressiveDocument відкриває файл, щойно байтів досить, а CancelProgressiveLoad полишає перерване завантаження без витоку нативних хендлів. Важка частина — не happy path. Переглядач на нестабільному з'єднанні побачить, як користувачі закривають вкладку на 25 відсотках, передумають і відкривають те саме посилання знову, і кожна з тих перерваних сесій має нативний availability-хендл, два записи C-callback, адаптер потоку і набір range-запитів у польоті, які треба вивільнити в точно правильному порядку
Як TPdfProgressiveDocument завантажує PDF, який ще качається?
TPdfProgressiveDocument тримає PDFium availability-провайдера живим, поки потік із довільним доступом наповнюється, і питає того провайдера перед кожним кроком розбору, чи присутні потрібні йому байти. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) бере потік-оснування плюс логічний розмір віддаленого файлу, вмикає callback IsDataAvail і callback AddSegment у два записи і викликає FPDFAvail_Create. Коли PDFium питає, чи присутній діапазон, компонент відповідає так, якщо діапазон лежить усередині суцільного префікса, описаного AvailableByteCount, чи усередині діапазону, вже завершеного через планувальник RangeRequests, а подія OnDataAvailable може перекреслити вердикт для розріджених сховищ. Кожен виклик CheckDocumentAvailability повертає одне з трьох значень TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) і віддає назад діапазони, про які питав PDFium, як відсортований, злитий масив TPdfDownloadRanges, уже в черзі планувальника з пріоритетом rrpImmediate
// FetchRange — ваш транспорт (HTTP Range GET, сокет, читач blob-ів):
// він пише 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. Порядок важить, бо планувальник серіалізує власний стан критичною секцією, але нічого не робить для TStream.Position на потоці-оснуванні: транспортний потік мусить записати байти відповіді в потік перед викликом CompleteRequest, бо в мить, щойно завершення опубліковано, PDFium може читати той діапазон, а конкурентні писальники потребують positioned I/O чи власного замка
Чому AvailableByteCount відмовляється рухатися назад?
AvailableByteCount лише росте, і сетер підіймає EPdfError із «Available byte count cannot move backwards», коли ви пробуєте його зменшити. Щойно callback IsDataAvail сказав PDFium, що діапазон існує, парсер міг уже прочитати і закешувати звідти об'єкти, тож відкликання тих байтів потім зробило б відповіді availability неузгодженими з тим, що PDFium уже спожив. Той самий сетер відкидає значення більші за LogicalFileSize і підіймає «No progressive load is active» поза сесією, — саме тому байти, які ви вже тримаєте до старту завантаження, належать аргументу AInitialAvailableByteCount у BeginProgressiveLoad, а не присвоєнню властивості, зробленому занадто рано. Якщо ваше сховище завантаження наповнюється безладно, не пробуйте виражати це через префікс узагалі: завершуйте діапазони через планувальник чи відповідайте через OnDataAvailable
Коли частково завантажений PDF справді може відкритися?
Лише лінеаризований PDF (Annex F ISO 32000-1, layout «Fast Web View») відкривається до прибуття всього файлу; нелінеаризований PDF досі потребує кожного байта. OpenProgressiveDocument перевіряє властивість Linearization (plnUnknown, plnNotLinearized, plnLinearized) і маршрутизує відповідно: лінеаризований файл відкривається через FPDFAvail_GetDocument, щойно секція першої сторінки і hint-таблиці присутні, тоді як нелінеаризований відкривається через FPDF_LoadCustomDocument на тому самому записі файлового доступу і трактується як читабельний лише цілим. Маршрутизація існує за конкретної причини. Виклик FPDFAvail_GetDocument на нелінеаризованому файлі може повернути ненульовий хендл, чия кількість сторінок — нуль: документ, що виглядає відкритим і є порожнім. У власному тестовому наборі компонента лінеаризована фікстура на 51 сторінку досягає pdaAvailable і відкривається з повним деревом сторінок, поки розріджене сховище завантаження досі не покриває файл
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 і забезпечує порядок, якого чекає PDFium: перед першою перевіркою сторінки він виконує CheckFormAvailability, що обгортає FPDFAvail_IsFormAvail, і лише після того викликає FPDFAvail_IsPageAvail. Результат pfaNotPresent — нормальна відповідь для документа без AcroForm і нічого не блокує. Коли сторінка готова, LoadAvailablePage робить її активною, тож переглядач може рендерити сторінку 1 лінеаризованої брошури, поки решта сторінок ще в дорозі; FirstAvailablePageNumber каже, яку сторінку словник лінеаризації призначає першою, — уже конвертовано з 0-based індексу PDFium
Що вивільняє CancelProgressiveLoad, і в якому порядку?
CancelProgressiveLoad розбирає сесію за чотири кроки, які не можна переставляти: скасуйте планувальник діапазонів, закрийте документ, знищте availability-хендл через FPDFAvail_Destroy, потім утилізуйте записи callback і звільніть адаптер потоку. Скасування планувальника першим підіймає його лічильник поколінь, скидає кожен запит — очікуваний і в польоті — і стріляє OnCancelRequest для кожного в польоті, тож завершення транспорту, що приземлиться пізніше, несе старе покоління, і CompleteRequest повертає False, нічого не торкаючись. Документ мусить закритися до того, як зникнуть availability-хендл і адаптер, бо PDFium може робити callback у провайдера файлового доступу, поки закриває документ, і якщо адаптера вже немає, той callback читає звільнену пам'ять
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Планувальник живе стільки ж, скільки FPdf, тож підключіть його раз
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // ваш код: закрийте той сокет чи запит
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Метод ідемпотентний і є єдиним шляхом прибирання для трьох ситуацій: BeginProgressiveLoad, що провалився на півдорозі конструювання, явне скасування користувачем і деструктор. BeginProgressiveLoad також викликає його перед стартом, тож перезапуск того самого об'єкта на новому URL безпечний без явного скасування. Одне рішення про володіння — ваше, і його треба зробити правильно: якщо робочий потік пише в потік-оснування, передайте AOwnsStream = False і звільніть потік самі після зупинки робочого потоку, бо з переданим володінням cancel звільнить потік, поки пізній запис може бути ще в дорозі. Винятки, підняті всередині OnCancelRequest, ковтаються по одному на запит, тож один транспорт, що провалився, не може заблокувати решту скасувань
Як набір lifecycle-тестів доводить, що шлях cancel не тікає?
Стрес-набір lifecycle у PDFium Component відпрацьовує перерване мережеподібне завантаження на кожному змішаному циклі. Кожен цикл стартує progressive load, чиє сховище тримає лише чверть байтів фікстури, вимагає pdaNotAvailable з непорожнім списком підказок, викликає CancelProgressiveLoad і асьертить, що об'єкт звітує ані ProgressiveLoading, ані Active; потім він ганяє той самий streaming-шлях до завершення з повною availability, OpenProgressiveDocument, рендером і закриттям. Типовий змішаний прогін покриває 100 виміряних циклів з 600 відкриттями, 2300 рендерами і 100 progressive-скасуваннями, а семпльована приватна пам'ять зросла на 8,21 МіБ проти бюджету 32 МіБ. Набір рахує progressive-скасування окремо від скасувань render-callback, бо перерване завантаження і render-петля, що зупиняється рано, — різні події з різними критеріями приймання
Де progressive-шлях перестає помагати
Кілька меж варто знати, перш ніж будувати переглядач поверх цього. Можливості, яким потрібні байти оригінального файлу, відмовляють progressive-джерелу, якщо воно неповне, замість вгадувати: ReadXmpPacket провалюється явно, а валідація підписів звітує Indeterminate, доки весь файл не присутній. Типовий тест availability припускає суцільний префікс, тож транспорт, що тягне діапазони безладно, мусить завершувати їх через RangeRequests чи відповідати через OnDataAvailable, інакше PDFium питатиме байти, які ви вже тримаєте. Нелінеаризований файл не виграє нічого в часі до першої сторінки, тож якщо швидкий перший рендер важить, лінеаризуйте файл на сервері. І CancelProgressiveLoad сам не закриває ваші сокети; OnCancelRequest — хук, де це відбувається
Для простого шляху stream-адаптера, що вантажить повний локальний файл на вимогу, дивіться статтю про streaming великих PDF на вимогу з PDFium; про відкриття PDF, що сидить усередині більшого буфера, — завантаження byte range для вбудованих PDF. Скасування повільного рендера вже завантаженої сторінки — окремий механізм, розібраний у скасовуваному progressive-рендерингу сторінок. TPdfProgressiveDocument і його планувальник діапазонів виходять з PDFium Component для Delphi і C++Builder