PDFium Component פותח PDF שעדיין בהורדה דרך TPdfProgressiveDocument, תת-מחלקה של TPdf שעוטפת את ה-API של הזמינות FPDFAvail_* של PDFium. BeginProgressiveLoad פותח את הסשן, CheckDocumentAvailability מדווח אילו טווחי בייטים PDFium עוד צריך, OpenProgressiveDocument פותח את הקובץ ברגע שיש מספיק בייטים, ו-CancelProgressiveLoad נוטש הורדה שנקטעה בלי לדלוף handles נייטיביים. הקושי הוא לא הדרך הפורחת. viewer על חיבור רעוע יראה משתמשים שסוגרים את הטאב ב-25 אחוזים, מתחרטים ופותחים שוב את אותו קישור, ולכל אחד מהסשנים הנטושים האלה יש handle זמינות נייטיבי, שני רקורדי callback של C, מתאם stream וקבוצה של בקשות טווח בדרך שחייבות להשתחרר בסדר המדויק הנכון
איך TPdfProgressiveDocument טוען PDF שעדיין בהורדה?
TPdfProgressiveDocument משאיר provider זמינות של PDFium בחיים בזמן ש-stream של גישה אקראית מתמלא, ושואל את ה-provider לפני כל שלב פרסור אם הבייטים שהוא רוצה קיימים. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) מקבל את ה-stream התומך בתוספת הגודל הלוגי של הקובץ המרוחק, מחבר callback של IsDataAvail ו-callback של AddSegment אל שני רקורדים, וקורא ל-FPDFAvail_Create. כש-PDFium שואל אם טווח קיים, הרכיב עונה כן אם הטווח נופל בתוך הקידומת הרציפה ש-AvailableByteCount מתאר או בתוך טווח שכבר הושלם דרך המתזמן של RangeRequests, ואירוע ה-OnDataAvailable יכול לעקוף את פסק הדין עבור חנויות דלילות. כל קריאה ל-CheckDocumentAvailability מחזירה אחד משלושת ערכי TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) ומוסרת את הטווחים ש-PDFium ביקש בתור מערך TPdfDownloadRanges ממוין וממוזג, שכבר נתון בתור במתזמן בעדיפות rrpImmediate
// FetchRange הוא הטרנספורט שלכם (HTTP Range GET, socket, קורא 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 לבקש את אותם טווחים נצח, ולולאה בלתי חסומה הופכת כישלון רשת לממשק תלוי. הסדר חשוב כי המתזמן מסדר בטור את המצב שלו עם critical section אבל לא עושה דבר לגבי TStream.Position על החנות התומכת: thread של טרנספורט חייב לכתוב את בייטי התשובה אל ה-stream לפני שהוא קורא ל-CompleteRequest, כי ברגע שהשלמה מתפרסמת PDFium עשוי לקרוא את הטווח הזה, וכותבים מקבילים זקוקים ל-I/O ממוקם או למנעול משלהם
למה AvailableByteCount מסרב לצעוד אחורה?
AvailableByteCount רק גדל, וה-setter מעלה EPdfError עם "Available byte count cannot move backwards" כשמנסים לכווץ אותו. ברגע שה-callback של IsDataAvail אמר ל-PDFium שטווח קיים, המפרסר ייתכן שכבר קרא ושמר אובייקטים ממנו, ולכן משיכת הבייטים האלה אחר כך הייתה עושה את תשובות הזמינות לא עקביות עם מה ש-PDFium כבר צרך. אותו setter דוחה גם ערכים גדולים מ-LogicalFileSize ומעלה "No progressive load is active" מחוץ לסשן, וזו הסיבה שבייטים שכבר מחזיקים בהם לפני תחילת הטעינה שייכים לארגומנט AInitialAvailableByteCount של BeginProgressiveLoad ולא להצבה מוקדמת מדי במאפיין. אם חנות ההורדה שלכם מתמלאת שלא בסדר, אל תנסו לבטא את זה דרך הקידומת בכלל: משלימים את הטווחים דרך המתזמן של RangeRequests או עונים דרך OnDataAvailable
מתי PDF שהורד חלקית באמת נפתח?
רק PDF מלינאר (Annex F של ISO 32000-1, הפריסה של "Fast Web View") נפתח לפני שהקובץ כולו הגיע; PDF שאינו מלינאר עדיין זקוק לכל בייט. OpenProgressiveDocument בודק את המאפיין Linearization (plnUnknown, plnNotLinearized, plnLinearized) ומנתב בהתאם: קובץ מלינאר נפתח דרך FPDFAvail_GetDocument ברגע שקטע העמוד הראשון וטבלאות הרמז קיימים, בזמן שקובץ לא מלינאר נפתח דרך FPDF_LoadCustomDocument על אותו רקורד גישה לקובץ ומתייחסים אליו כקריא רק כשלם. לניתוב יש סיבה מוחשית. קריאה ל-FPDFAvail_GetDocument על קובץ לא מלינאר יכולה להחזיר handle שאינו null שספירת העמודים שלו אפס, מסמך שנראה פתוח והוא ריק. בסוויטת הבדיקות של הרכיב עצמו, fixture מלינאר של 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 הופך אותו לעמוד הפעיל, כך ש-viewer יכול לצייר את עמוד 1 של עלון מלינאר בזמן ששאר העמודים עוד בדרך; FirstAvailablePageNumber אומר לכם איזה עמוד מילון הלינאריזציה מייעד כראשון, כבר מומר מהאינדקס מבוסס-האפס של PDFium
מה CancelProgressiveLoad משחרר, ובאיזה סדר?
CancelProgressiveLoad מפרק סשן בארבעה שלבים שאי אפשר לסדר מחדש: מבטל את מתזמן הטווחים, סוגר את המסמך, משמיד את handle הזמינות עם FPDFAvail_Destroy, ואז מפנה את רקורדי ה-callback ומשחרר את מתאם ה-stream. ביטול המתזמן קודם מקפיץ את מונה ה-generation שלו, משליך כל בקשה ממתינה ובדרך, ומפעיל את OnCancelRequest עבור כל אחת מאלה שבדרך, כך שהשלמה של טרנספורט שתנחת אחר כך נושאת את ה-generation הישן ו-CompleteRequest מחזיר False בלי לגעת בשום דבר. המסמך חייב להיסגר לפני ש-handle הזמינות והמתאם הולכים לאיבוד, כי PDFium עשוי לקרוא חזרה אל provider הגישה לקובץ בזמן שהוא סוגר מסמך, ואם המתאם כבר איננו, ה-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); // הקוד שלכם: לסגור את ה-socket או הבקשה ההיא
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
השיטה idempotent והיא נתיב הניקוי היחיד לשלושה מצבים: BeginProgressiveLoad שנכשל באמצע הבנייה, ביטול מפורש של משתמש, וה-destructor. BeginProgressiveLoad קורא לה גם לפני ההתחלה, כך שהפעלה מחדש של אותו אובייקט על URL חדש בטוחה בלי ביטול מפורש. החלטת בעלות אחת נתונה לכם להכריע נכון: אם worker thread כותב אל ה-stream התומך, העבירו AOwnsStream = False ושחררו את ה-stream בעצמכם אחרי שה-worker נעצר, כי עם בעלות שהועברה הביטול משחרר את ה-stream בזמן שכתיבה מאוחרת עדיין עשויה להיות בדרך. חריגות שמועלות בתוך OnCancelRequest נבלעות לכל בקשה, כך שטרנספורט אחד שנכשל לא יכול לחסום את שאר הביטולים
איך סוויטת מחזור החיים מוכיחה שנתיב הביטול לא מדליף?
סוויטת המאמץ של מחזור החיים של PDFium Component מתאמנת הורדה נקטעת בסטיל רשת על כל מחזור מעורב. כל מחזור פותח טעינה הדרגתית שהחנות שלה מחזיקה רק רבע מבייטי ה-fixture, דורש pdaNotAvailable עם רשימת רמזים לא ריקה, קורא ל-CancelProgressiveLoad, ומוודא שהאובייקט לא מדווח גם ProgressiveLoading וגם Active; ואז הוא מריץ את אותו נתיב סטרימינג עד הסוף עם זמינות מלאה, OpenProgressiveDocument, ציור וסגירה. הריצה המעורבת ברירת המחדל מכסה 100 מחזורים מדודים עם 600 פתיחות, 2300 ציורים ו-100 ביטולים הדרגתיים, והזיכרון הפרטי הנדגם גדל ב-8.21 MiB מול תקציב של 32 MiB. הסוויטה סופרת ביטולים הדרגתיים בנפרד מביטולי callback של ציור, כי הורדה שנקטעה ולולאת ציור שנעצרת מוקדם הם אירועים שונים עם קריטריוני קבלה שונים
איפה הנתיב ההדרגתי מפסיק לעזור
כמה מגבלות שוות היכרות לפני שבונים viewer על בסיס זה. יכולות שזקוקות לבייטי הקובץ המקורי מסרבות למקור הדרגתי חלקי במקום לנחש: ReadXmpPacket נכשל במפורש ואימות חתימות מדווח Indeterminate עד שהקובץ כולו קיים. בדיקת הזמינות ברירת המחדל מניחה קידומת רציפה, ולכן טרנספורט שמוריד טווחים שלא בסדר חייב להשלים אותם דרך RangeRequests או לענות דרך OnDataAvailable, אחרת PDFium ימשיך לבקש בייטים שכבר מחזיקים בהם. קובץ לא מלינאר לא מרוויח דבר בזמן עד העמוד הראשון, ולכן אם ציור ראשון מהיר חשוב, לינארו את הקובץ בצד השרת. ו-CancelProgressiveLoad לא סוגר את ה-sockets שלכם בעצמו; OnCancelRequest הוא ה-hook שבו זה קורה
לנתיב מתאם ה-stream הפשוט שטוען קובץ מקומי שלם לפי דרישה, ראו סטרימינג של PDF גדולים לפי דרישה עם PDFium; לפתיחת PDF שיושב בתוך buffer גדול יותר, ראו טעינת byte range עבור PDFs מוטמעים. ביטול ציור איטי של עמוד שכבר נטען הוא מנגנון נפרד, המכוסה ב-ציור עמודים הדרגתי ניתן לביטול. את TPdfProgressiveDocument ואת מתזמן הטווחים שלו מקבלים יחד עם PDFium Component עבור Delphi ו-C++Builder