PDFium Component mở một PDF vẫn đang tải về qua TPdfProgressiveDocument, một lớp con của TPdf bọc API availability FPDFAvail_* của PDFium. BeginProgressiveLoad khởi động session, CheckDocumentAvailability báo cáo những vùng byte nào PDFium vẫn cần, OpenProgressiveDocument mở file khi đủ byte, và CancelProgressiveLoad bỏ một lần tải gián đoạn mà chẳng làm rò handle native. Phần khó không phải đường vui. Một viewer trên một kết nối bấp bênh sẽ thấy người dùng đóng tab ở mức 25 phần trăm, đổi ý, rồi mở lại cùng link đó, và mỗi session bỏ dở như vậy mang một availability handle native, hai record callback C, một stream adapter và một đống range request đang bay phải được giải phóng theo đúng thứ tự
TPdfProgressiveDocument nạp một PDF vẫn đang tải về thế nào?
TPdfProgressiveDocument giữ một availability provider của PDFium sống động trong khi một stream truy cập ngẫu nhiên được đổ đầy, và hỏi provider đó trước mỗi bước parse xem những byte nó muốn đã có mặt chưa. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) nhận stream nền cộng kích thước logic của file remote, nối một callback IsDataAvail và một callback AddSegment vào hai record, rồi gọi FPDFAvail_Create. Khi PDFium hỏi một range có hiện diện không, component trả lời có nếu range nằm bên trong prefix liền mạch mô tả bởi AvailableByteCount hay bên trong một range đã hoàn tất qua scheduler RangeRequests, còn event OnDataAvailable có thể phủ quyết phán quyết đó với các store rời rạc. Mỗi lần gọi CheckDocumentAvailability trả một trong ba giá trị TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) và đưa lại các range PDFium hỏi dưới dạng một mảng TPdfDownloadRanges đã sắp xếp, đã gộp, đã xếp hàng trên scheduler với ưu tiên rrpImmediate
// FetchRange là transport của bạn (HTTP Range GET, socket, blob reader):
// nó ghi Size byte tại Offset vào Store và trả về số byte đã tới
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;
// Các hint đã được xếp hàng; ghi byte trước, rồi hoàn tất
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;
Hai chi tiết trong vòng lặp đó đang gánh tải. Trần số vòng quan trọng vì một link chết khiến CheckDocumentAvailability hỏi đi hỏi lại cùng các range mãi mãi, và một vòng lặp không chặn biến một lỗi mạng thành một UI treo. Thứ tự quan trọng vì scheduler serial hóa trạng thái riêng của nó bằng một critical section nhưng chẳng làm gì cho TStream.Position của store nền: một thread transport phải ghi các byte response vào stream trước khi gọi CompleteRequest, vì ngay khoảnh khắc một lần hoàn tất được công bố, PDFium có thể đọc range đó, còn các writer đồng thời cần positioned I/O hay một khóa của riêng họ
Vì sao AvailableByteCount từ chối đi lùi?
AvailableByteCount chỉ tăng, và setter raise EPdfError kèm thông báo “Available byte count cannot move backwards” khi bạn thử thu nhỏ nó. Một khi callback IsDataAvail đã báo với PDFium rằng một range tồn tại, parser có thể đã đọc và cache các object từ đó, nên rút những byte đó đi về sau sẽ khiến các câu trả lời availability mâu thuẫn với những gì PDFium đã tiêu thụ. Cùng setter đó từ chối các giá trị lớn hơn LogicalFileSize và raise “No progressive load is active” ngoài session, đó là lý do các byte bạn đã nắm giữ trước khi tải bắt đầu thuộc về tham số AInitialAvailableByteCount của BeginProgressiveLoad thay vì một phép gán property quá sớm. Nếu store tải về của bạn đầy theo thứ tự lộn xộn, đừng cố diễn đạt điều đó qua prefix: hãy hoàn tất các range qua scheduler hay trả lời qua OnDataAvailable
Một PDF tải dở khi nào mới thực sự mở được?
Chỉ một PDF linearized (Phụ lục F của ISO 32000-1, bố cục “Fast Web View”) mới mở được trước khi toàn bộ file tới nơi; một PDF không linearized vẫn cần mọi byte. OpenProgressiveDocument kiểm property Linearization (plnUnknown, plnNotLinearized, plnLinearized) và điều hướng tương ứng: một file linearized mở qua FPDFAvail_GetDocument ngay khi phần first-page và các bảng hint có mặt, trong khi một file không linearized được mở qua FPDF_LoadCustomDocument trên cùng record truy cập file và chỉ được coi là đọc được khi nguyên vẹn. Sự điều hướng tồn tại vì một lý do cụ thể. Gọi FPDFAvail_GetDocument trên một file không linearized có thể trả một handle khác null mà số trang bằng 0, một tài liệu trông như mở mà rỗng tuếch. Trong test suite của chính component, một fixture linearized 51 trang đạt pdaAvailable và mở với page tree đầy đủ trong khi store tải rời rạc vẫn chưa phủ hết file
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 giờ là trang active
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage nhận một số trang tính từ 1 và thực thi thứ tự mà PDFium kỳ vọng: trước phép kiểm trang đầu tiên, nó chạy CheckFormAvailability, thứ bọc FPDFAvail_IsFormAvail, và chỉ sau đó nó mới gọi FPDFAvail_IsPageAvail. Một kết quả pfaNotPresent là câu trả lời bình thường cho một tài liệu không có AcroForm và chẳng chặn thứ gì. Khi trang đã sẵn sàng, LoadAvailablePage biến nó thành trang active, nên một viewer có thể render trang 1 của một cuốn brochure linearized trong khi các trang còn lại vẫn đang trên đường; FirstAvailablePageNumber cho biết trang nào được dictionary linearization chỉ định là trang đầu, đã chuyển từ chỉ mục zero-based của PDFium
CancelProgressiveLoad giải phóng gì, và theo thứ tự nào?
CancelProgressiveLoad dỡ một session trong bốn bước không thể đảo thứ tự: hủy range scheduler, đóng tài liệu, phá availability handle bằng FPDFAvail_Destroy, rồi dispose các record callback và free stream adapter. Hủy scheduler trước nâng bộ đếm generation của nó, vứt mọi request chờ và đang bay, và bắn OnCancelRequest cho từng cái đang bay, nên một lần hoàn tất transport đáp xuống muộn hơn sẽ mang generation cũ và CompleteRequest trả False mà chẳng đụng gì. Tài liệu phải đóng trước khi availability handle và adapter biến mất, vì PDFium có thể gọi ngược vào provider truy cập file trong lúc nó đóng một tài liệu, và nếu adapter đã biến mất thì callback đó đọc bộ nhớ đã free
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Scheduler sống cùng tuổi với FPdf, nên nối nó đúng một lần
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // code của bạn: đóng socket hay request đó
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Method này idempotent và là con đường dọn dẹp duy nhất cho ba tình huống: một BeginProgressiveLoad gãy giữa chừng lúc dựng, một lần hủy tường minh của người dùng, và destructor. BeginProgressiveLoad cũng gọi nó trước khi bắt đầu, nên khởi động lại cùng object trên một URL mới là an toàn mà chẳng cần hủy tường minh. Một quyết định sở hữu là của bạn và phải làm đúng: nếu một worker thread ghi vào stream nền, hãy truyền AOwnsStream = False và tự free stream sau khi worker đã dừng, vì khi quyền sở hữu được bàn giao, cancel sẽ free stream trong khi một cú ghi muộn vẫn có thể đang trên đường. Các exception raise bên trong OnCancelRequest bị nuốt theo từng request, để một transport gãy không thể chặn các lần hủy còn lại
Lifecycle suite chứng minh đường cancel không rò rỉ thế nào?
Lifecycle stress suite của PDFium Component thực hành một kiểu tải gián đoạn theo phong cách mạng trên mỗi mixed cycle. Mỗi cycle khởi động một progressive load mà store chỉ giữ một phần tư số byte của fixture, đòi pdaNotAvailable với một danh sách hint không rỗng, gọi CancelProgressiveLoad, và assert rằng object không báo ProgressiveLoading lẫn Active; rồi nó chạy cùng đường streaming đến hoàn tất với availability đầy đủ, OpenProgressiveDocument, một lần render và một lần đóng. Lượt mixed mặc định phủ 100 cycle đo được với 600 lần mở, 2300 lần render và 100 lần hủy progressive, và bộ nhớ riêng lấy mẫu tăng 8.21 MiB so với một trần 32 MiB. Suite đếm các lần hủy progressive tách khỏi các lần hủy render-callback, vì một lần tải bỏ dở và một vòng render dừng sớm là hai sự kiện khác nhau với các tiêu chí nghiệm thu khác nhau
Chỗ đường progressive ngừng giúp đỡ
Vài giới hạn đáng biết trước khi bạn dựng một viewer trên nền này. Các tính năng cần byte file gốc từ chối một nguồn progressive chưa hoàn chỉnh thay vì đoán: ReadXmpPacket gãy tường minh và validation chữ ký báo Indeterminate cho tới khi toàn bộ file có mặt. Phép kiểm availability mặc định giả định một prefix liền mạch, nên một transport fetch các range lộn xộn phải hoàn tất chúng qua RangeRequests hay trả lời qua OnDataAvailable, nếu không PDFium sẽ cứ hỏi mãi những byte bạn đã nắm giữ. Một file không linearized chẳng được gì về thời gian tới trang đầu, nên nếu tốc độ vẽ khung đầu quan trọng, hãy linearize file phía server. Và CancelProgressiveLoad không tự đóng socket của bạn; OnCancelRequest chính là hook cho việc đó
Với đường stream-adapter trơn nạp một file local hoàn chỉnh theo yêu cầu, xem streaming các PDF lớn theo yêu cầu với PDFium; với việc mở một PDF nằm trong một buffer lớn hơn, xem nạp byte range cho embedded PDF. Hủy một lần render chậm của một trang đã nạp là một cơ chế riêng, trình bày trong render trang progressive có thể hủy. TPdfProgressiveDocument và range scheduler của nó được giao kèm với PDFium Component for Delphi and C++Builder