يفتح مكوّن PDFium ملف PDF ما يزال ينزل عبر TPdfProgressiveDocument، وهي صنف فرعي من TPdf يغلّف واجهة التوفر FPDFAvail_* في PDFium. يبدأ BeginProgressiveLoad الجلسة، ويبلّغ CheckDocumentAvailability عن مداقات البايتات التي ما يزال PDFium يحتاجها، ويفتح OpenProgressiveDocument الملف متى وُجدت بايتات كافية، ويتخلى CancelProgressiveLoad عن تنزيل مقطوع دون تسريب مقابض أصلية. الصعب ليس المسار السعيد. عارض على اتصال مهتز سيرى مستخدمين يغلقون اللسان عند 25 بالمئة، ثم يغيرون رأيهم، ثم يفتحون الرابط نفسه ثانيةً، وكل جلسة مقطوعة من تلك تحمل مقبض توفر أصليًا وسجلَّي رد نداء C ومحوّل تدفق ومجموعة طلبات مدى جارية يجب تحريرها بالترتيب الصحيح تمامًا
كيف يحمّل TPdfProgressiveDocument ملف PDF ما يزال ينزل؟
يبقي TPdfProgressiveDocument مزود توفر PDFium حيًا بينما يمتلئ تدفق وصول عشوائي، ويسأل ذلك المزود قبل كل خطوة تحليل هل البايتات التي يريدها موجودة. تأخذ BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) التدفق الحامل زائد الحجم المنطقي للملف البعيد، وتوصّل رد نداء IsDataAvail ورد نداء AddSegment في سجلين، وتستدعي FPDFAvail_Create. حين يسأل PDFium هل مدى موجود، يجيب المكوّن بنعم إذا وقع المدى داخل البادئة المتصلة الموصوفة في AvailableByteCount أو داخل مدى اكتمل سلفًا عبر مجدول RangeRequests، ويمكن لحدث OnDataAvailable أن يتجاوز الحكم للمخازن المتفرقة. يعيد كل استدعاء لـ CheckDocumentAvailability إحدى ثلاث قيم من TPdfDataAvailability (pdaAvailable وpdaNotAvailable وpdaError) ويسلم المداقات التي طلبها PDFium بوصفها مصفوفة TPdfDownloadRanges مرتبة ومدمجة، مصفوفةً على المجدول سلفًا بأولوية rrpImmediate
// الدالة FetchRange هي ناقلك (طلب GET بمدى HTTP أو مقبس أو قارئ كتل):
// تكتب 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 تطلب المداقات نفسها إلى الأبد، فالحلقة بلا حد تحوّل فشل شبكة إلى واجهة معلقة. والترتيب مهم لأن المجدول يسلسل حالته الخاصة بمقطع حرج لكنه لا يفعل شيئًا لـ TStream.Position على المخزن الحامل: يجب على خيط النقل أن يكتب بايتات الرد في التدفق قبل استدعاء CompleteRequest، لأن لحظة نشر الإكمال قد يقرأ فيها PDFium ذلك المدى، وعلى الكتّاب المتوازيين إدخالًا متموضعًا أو قفلًا خاصًا بهم
لماذا يرفض AvailableByteCount الرجوع إلى الخلف؟
AvailableByteCount ينمو فقط، ويرفع الواضع استثناء EPdfError بالرسالة "Available byte count cannot move backwards" حين تحاول تقليصه. متى أخبر رد نداء IsDataAvail بنية PDFium أن مدى موجود، ربما كان المحلل قد قرأ منه كائنات وخبأها، فسحب تلك البايتات لاحقًا سيجعل أجوبة التوفر تناقض ما استهلكه PDFium سلفًا. ويرفض الواضع نفسه القيم الأكبر من LogicalFileSize ويرفع "No progressive load is active" خارج جلسة، ولهذا البايتات التي تحملها قبل بدء التحميل مكانها وسيط AInitialAvailableByteCount في BeginProgressiveLoad لا إسناد خاصية مبكر. وإن امتلأ مخزن التنزيل عندك بلا ترتيب، فلا تحاول التعبير عن ذلك عبر البادئة أصلًا: أكمل المداقات عبر المجدول أو أجب عبر OnDataAvailable
متى يفتح ملف PDF منزّل جزئيًا فعلًا؟
لا يفتح قبل وصول الملف كله إلا PDF مخطط (الملحق F من ISO 32000-1، أي تخطيط Fast Web View)؛ وما دون ذلك من الملفات يحتاج كل بايت. يفحص OpenProgressiveDocument خاصية Linearization (plnUnknown وplnNotLinearized وplnLinearized) ويوجه وفقها: الملف المخطط يفتح عبر FPDFAvail_GetDocument متى وُجد مقطع الصفحة الأولى وجداول التلميحات، بينما يفتح الملف غير المخطط عبر 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 رقم صفحة يبدأ من الواحد وتفرض الترتيب الذي يتوقعه PDFium: قبل أول فحص صفحة تجري CheckFormAvailability، التي تغلّف FPDFAvail_IsFormAvail، وبعدها فقط تستدعي FPDFAvail_IsPageAvail. ونتيجة pfaNotPresent هي الجواب الطبيعي لمستند بلا AcroForm ولا تسد طريق شيء. وحين تصبح الصفحة جاهزة تجعلها LoadAvailablePage الصفحة النشطة، فيستطيع عارض رسم الصفحة 1 من كتيب مخطط بينما الصفحات الباقية ما تزال في الطريق؛ ويخبرك FirstAvailablePageNumber أي صفحة يعينها قاموس التخطيط أولى، محولًا سلفًا من فهرس PDFium القائم على الصفر
ماذا يحرر CancelProgressiveLoad، وبأي ترتيب؟
تفك CancelProgressiveLoad الجلسة في أربع خطوات لا تحتمل إعادة الترتيب: ألغِ مجدول المدى، وأغلق المستند، ودمّر مقبض التوفر عبر FPDFAvail_Destroy، ثم تخلص من سجلات ردود النداء وحرر محوّل التدفق. إلغاء المجدول أولًا يرفع عداد توليده، ويسقط كل طلب معلق وجارٍ، ويُطلق OnCancelRequest لكل طلب جارٍ، فإكمال ناقل يهبط لاحقًا يحمل التوليد القديم وتعيد CompleteRequest القيمة False دون أن تلمس شيئًا. وعلى المستند أن يغلق قبل زوال مقبض التوفر والمحوّل لأن PDFium قد يستدعي مزود الوصول إلى الملف وهو يغلق مستندًا، فإن كان المحوّل قد زال قرأ ذلك النداء ذاكرة محررة
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 وحرر التدفق بنفسك بعد توقف العامل، لأنه مع تسليم الملكية يحرر الإلغاء التدفق بينما قد تكون كتابة متأخرة في الطريق. أما الاستثناءات المرفوعة داخل OnCancelRequest فتُبتلع لكل طلب على حدة كي لا يسد ناقل فاشل بقية الإلغاءات
كيف تثبت جناح دورة الحياة أن مسار الإلغاء لا يسرب؟
يجنح جناح إجهاد دورة الحياة في مكوّن PDFium تنزيلًا مقطوعًا بنمط شبكي في كل دورة مختلطة. تبدأ كل دورة تحميلًا تدريجيًا لا يحمل مخزنه إلا ربع بايتات الثابتة، وتشترط pdaNotAvailable مع قائمة تلميحات غير فارغة، وتستدعي CancelProgressiveLoad، وتتحقق أن الكائن لا يبلّغ عن ProgressiveLoading ولا عن Active؛ ثم تجري مسار البث نفسه حتى النهاية بتوفر كامل وOpenProgressiveDocument ورسم وإغلاق. تغطي الجولة المختلطة الافتراضية 100 دورة مقيسة بـ 600 فتح و2300 رسم و100 إلغاء تدريجي، ونما ذاكرة خاصة بالعيّنة 8.21 MiB مقابل ميزانية 32 MiB. ويعد الجناح الإلغاءات التدريجية على حدة عن إلغاءات ردود نداء الرسم، لأن تنزيلًا مقطوعًا وحلقة رسم تتوقف مبكرًا حدثان مختلفان بمعايير قبول مختلفة
أين يتوقف المسار التدريجي عن الإفادة
بضعة حدود يستحق معرفتها قبل أن تبني عارضًا فوق هذا. الميزات التي تحتاج بايتات الملف الأصلية ترفض مصدرًا تدريجيًا ناقصًا بدل التخمين: تفشل ReadXmpPacket فشلًا صريحًا ويبلّغ التحقق من التواقيع عن Indeterminate حتى يكتمل الملف. يفترض فحص التوفر الافتراضي بادئة متصلة، فناقل يجلب المداقات بلا ترتيب عليه أن يكملها عبر RangeRequests أو يجيب عبر OnDataAvailable، وإلا واصل PDFium طلب بايتات تحملها سلفًا. والملف غير المخطط لا يكسب شيئًا في زمن أول صفحة، فإن كان أول رسم سريع مهمًا فخطط الملف على جانب الخادم. وCancelProgressiveLoad لا يغلق مقابسك الشبكية بنفسه؛ وOnCancelRequest هو الخطاف الذي يحدث فيه ذلك
لمسار محوّل التدفق المجرد الذي يحمّل ملفًا محليًا كاملًا عند الطلب، انظر بث ملفات PDF الكبيرة عند الطلب مع PDFium؛ ولفتح PDF يسكن داخل مخزن أكبر، انظر تحميل مدى البايتات لملفات PDF المضمنة. وإلغاء رسم بطيء لصفحة محملة سلفًا آلية منفصلة، مغطاة في رسم الصفحات التدريجي القابل للإلغاء. تُشحن TPdfProgressiveDocument ومجدول مداياتها مع PDFium Component لـ Delphi وC++Builder