يمكن أن يصل الأرشيف الممسوح ضوئياً إلى عدة جيجابايت في ملف PDF واحد. عادة ما يرغب العارض الذي يفتح مثل هذا الملف في عرض صفحة واحدة، ربما جدول المحتويات، أو ربما صفحة قفز إليها المستخدم من إشارة مرجعية. إن قراءة الملف بأكمله في الذاكرة لرسم صفحتين يعد إهداراً على كل المستويات: فهو يستهلك مساحة العنوان، ويعطل المستخدم خلف قراءة أولية طويلة، وفي عملية Delphi 32 بت يمكن أن يفشل تماماً قبل ظهور صفحة واحدة. تم بناء PDFium مع أخذ ذلك في الاعتبار. يمكنه تحميل مستند من خلال استدعاء يطلب نطاقات بايت محددة يحتاجها، عندما يحتاجها، ولا يطلب أبداً الملف بأكمله دفعة واحدة.
يكشف المكون عن هذا المسار من خلال محول تدفق (stream adapter). تقوم بتسليمه أي TStream، ويسحب PDFium الكتل من هذا التدفق عند الطلب. يمكن للملف أن يقع على القرص، أو في حقل كائن ثنائي كبير (blob) لقاعدة البيانات، أو خلف أي سليل آخر لـ TStream، ولا يتم نسخ أي منه في الذاكرة مقدماً.
كيف يطلب PDFium البايتات
تقوم واجهة برمجة تطبيقات C لـ PDFium بتحميل مستند من كائن يقدمه المستدعي والموصوف بواسطة بنية FPDF_FILEACCESS. تحتوي البنية على ثلاثة أجزاء تهمنا هنا: حقل طول، واستدعاء قراءة، ومعلمة مستخدم معتمة. نقطة الدخول التي تستهلكها هي FPDF_LoadCustomDocument. بمجرد أن يحتفظ PDFium بهذه البنية، فإنه يحلل الجزء الأخير من الملف، ويحدد موقع جدول المراجع التقاطعية، ومنذ ذلك الحين فصاعداً يقرأ فقط ما تتطلبه عملية معينة. لمس ذيل الملف وبضعة كائنات فهرس عند فتح المستند. ويقرأ رسم الصفحة 400 تدفقات المحتوى والموارد لتلك الصفحة ولا شيء غير ذلك.
هذا هو الفرق بين التحميل المخزن مؤقتاً والتحميل المتدفق. يقرأ التحميل المخزن مؤقتاً الملف من البداية إلى النهاية قبل أن يرى PDFium البايت صفر. يعكس التحميل المتدفق العلاقة: يقود PDFium عمليات القراءة، والبايتات التي لا يتم لمسها مطلقاً لا يتم قراءتها أبداً. بالنسبة لملف متعدد الجيجابايت يتم عرضه صفحة صفحة، هذه هي الفجوة بين تحميل غير قابل للاستخدام وتحميل فوري.
محول التدفق
المحول الذي يربط Delphi TStream بـ FPDF_FILEACCESS هو TPdfStreamAdapter. يأخذ منشئه التدفق وعلامة الملكية، ويلتقط طول التدفق مرة واحدة، ويملأ سجل FPDF_FILEACCESS، ويربط استدعاء القراءة. عندما يستدعي PDFium لاحقاً بإزاحة وحجم، يبحث المحول في التدفق عن تلك الإزاحة وينسخ هذا النطاق بالضبط في الحاوية التي قدمها PDFium.
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
تحدد علامة الملكية من يقوم بتحرير التدفق. مرر False ويحتفظ المستدعي بالتدفق ويجب أن يبقيه نشطاً طوال عمر المستند. مرر True ويتولى المحول المسؤولية، ويحرر التدفق عند إغلاق المستند. وفي كلتا الحالتين، يجب أن يعيش التدفق لفترة أطول من كل قراءة سيقوم بها PDFium، لأن PDFium يحتفظ بمؤشر FPDF_FILEACCESS وسيستدعي في أي نقطة أثناء فتح المستند، وليس فقط أثناء التحميل الأولي.
لماذا الاستدعاء هو دالة ثابتة
استدعاء القراءة الذي يخزنه PDFium في m_GetBlock هو مؤشر دالة C بسيط مع اتفاقية استدعاء cdecl. لا يمكن استخدام طريقة Delphi مباشرة، لأن الطريقة تحمل وسيطة Self مخفية لا يعرف مستدعي C شيئاً عنها ولن يقدمها أبداً. لذلك يعلن المحول عن الاستدعاء كـ class function مميزة بـ cdecl; static، والتي تتجمع إلى دالة مستقلة مع تخطيط إطار C الذي يتوقعه PDFium ودون Self ضمنية.
يحل هذا اتفاقية الاستدعاء ولكنه يثير سؤالاً ثانياً: مع عدم وجود Self، كيف يصل الاستدعاء إلى التدفق المحدد الذي يفترض أن يقرأ منه؟ الإجابة هي معلمة المستخدم المعتمة. عندما يبني المحول السجل فإنه يخزن مؤشر المثيل الخاص به في m_Param. يسلم PDFium نفس المؤشر كمعلمة أولى لكل استدعاء. تقوم الدالة الثابتة بتحويله مرة أخرى إلى TPdfStreamAdapter وترسل القراءة مقابل تدفق هذا المثيل. هذا هو الترامبولين القياسي لتسليم سياق الكائن عبر حدود C التي ليس لديها فكرة عن الكائنات.
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
سقف 4 جيجابايت ولماذا يحتاج إلى حارس
حقل الطول m_FileLen في FPDF_FILEACCESS هو قيمة غير موقعة 32 بت. أكبر طول يمكن تمثيله هو بايت واحد أقل من 4 جيجابايت. يبلغ TStream عن حجمه كـ Int64، لذا يمكن للتدفق وصف بايتات أكثر بكثير مما يمكن للحقل الاحتفاظ به. في اللحظة التي يتجاوز فيها حجم التدفق هذا السقف، لا توجد طريقة صادقة لإخبار PDFium بمدى طول الملف.
الاستجابة الخاطئة هي تعيين الحجم وتركه يلتف. إن اقتطاع طول 5 جيجابايت إلى حقل 32 بت ينتج رقماً صغيراً يبدو معقولاً، وسيقوم PDFium بعد ذلك بتحليل الملف معتقداً أنه ينتهي عند حوالي جيجابايت واحد. يعيش الجزء الأخير وجدول المراجع التقاطعية في النهاية الحقيقية للملف، متجاوزين بكثير الطول المقتطع، لذا يفشل التحليل بطريقة لا علاقة لها بالسبب الفعلي. ستكون بصدد تصحيح خطأ مرجعي تقاطعي في ملف صالح تماماً، دون أي تلميح إلى أن عدداً صحيحاً قد التف طبقتين أعلى.
يرفض المحول المدخلات بدلاً من ذلك. يقارن المنشئ حجم التدفق بـ High(FPDF_DWORD) ويثير EPdfError في اللحظة التي يكون فيها التدفق كبيراً جداً بحيث لا يمكن وصفه. يذكر الخطأ الصريح والفوري المشكلة الحقيقية عند نقطة البناء. ويخفيه الاقتطاع الصامت خلف أعراض مضللة ستطاردها لاحقاً. حد 4 جيجابايت هو قيد حقيقي لمسار التحميل هذا، والشيء الصادق هو إظهاره بصوت عالٍ بدلاً من التغطية عليه بعمليات حسابية تتصادف تجميعها.
يجب ألا تعبر الإخفاقات الحدود
يمكن أن تفشل القراءة. قد يكون التدفق عبارة عن كائن مدعوم بالشبكة تنتهي مهلته، أو مقبض blob تم إغلاقه تحتك، أو ملفاً تم اقتطاعه بعد فتح المستند. عقد PDFium لاستدعاء القراءة هو قيمة إرجاع: غير صفرية للنجاح، وصفر للفشل. إنه إطار C، ولا يملك آلية لالتقاط أو نشر استثناء Pascal.
هذا هو السبب في أن الترامبولين يغلف البحث والقراءة في try/except تبتلع الاستثناء وتعيد صفراً. إذا سُمح باستثناء Delphi بالانتشار خارج الاستدعاء، فإنه سيتم فكه عبر إطارات مكدس cdecl لـ PDFium، والتي لم يتم بناؤها أبداً ليتم فكها بواسطة آلية استثناءات Pascal. النتيجة هي سلوك غير محدد في أفضل الأحوال وانهيار شديد في أسوأ الأحوال، في أعماق محلل PDF دون مكدس قابل للاستخدام. تحافظ إعادة الصفر على الفشل داخل العقد. يرى PDFium قراءة كتلة فاشلة، ويلغي العملية بشكل نظيف، ويبلغ FPDF_LoadCustomDocument بأنه تعذر تحميل المستند، وهو ما يظهره المكون كـ EPdfError على جانب Pascal حيث ينتمي.
فتح مستند بهذه الطريقة
طريقة المكون التي تقود مسار التدفق هي LoadCustomDocument، المعلن عنها كطريقة مميزة بدلاً من تحميل زائد آخر لـ LoadDocument بحيث لا يؤدي تمرير TMemoryStream أبداً بالصدفة إلى الهبوط في المسار المخزن مؤقتاً. يقوم ببناء المحول، ويستدعي FPDF_LoadCustomDocument، ويبقي المحول نشطاً طوال عمر المستند المحمل.
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
يعمل نفس الاستدعاء لـ TMemoryStream، أو تدفق blob من مجموعة بيانات قاعدة البيانات، أو سليل مخصص لـ TStream. يستحق التحميل عند الطلب عناءه عندما يكون الملف كبيراً ويتم قراءة جزء منه فقط: مثل عارض الأرشيف، أو مولد الصور المصغرة الذي يأخذ عينات من بضع صفحات، أو فهرس البحث الذي يسحب صفحة واحدة في كل مرة. عندما يكون الملف صغيراً أو ستقرأه بالكامل على أي حال، يكون التحميل المخزن مؤقتاً أبسط ولا تشتري لك آلية التدفق شيئاً. العامل الحاسم هو نسبة البايتات التي ستلمسها بالفعل إلى البايتات التي يحتوي عليها الملف.
بمجرد تدفق الصفحات عند الطلب، فإن الشاغل التالي هو الحفاظ على استجابة الصفحات المرسومة أثناء قيام المستخدم بالتكبير والتمرير، وهو ما تمت تغطيته في ملاحظتنا حول أداء ذاكرة التخزين المؤقت للرسم والتكبير. عندما يكون المستند المتدفق مستنداً يجب على العارض عرضه ولكن لا يسمح للمستخدم بتصديره أو تعديله، فإن التقنيات في دليل معاينة PDF الآمنة تقترن بشكل طبيعي مع مسار التحميل هذا. كلاهما يبنى على التحميل المتدفق الموصوف هنا، والذي يتم شحنه كجزء من مكون PDFium لـ Delphi و C++Builder جنباً إلى جنب مع واجهات برمجة التطبيقات للرسم واستخراج النصوص والتعليقات التوضيحية المغطاة في مكان آخر في هذه المدونة.